Warning, /multimedia/elisa/src/qml/ContextView.qml is written in an unsupported language. File is not indexed.

0001 /*
0002    SPDX-FileCopyrightText: 2016 (c) Matthieu Gallien <matthieu_gallien@yahoo.fr>
0003    SPDX-FileCopyrightText: 2019 (c) Nate Graham <nate@kde.org>
0004 
0005    SPDX-License-Identifier: LGPL-3.0-or-later
0006  */
0007 
0008 import QtQuick 2.15
0009 import QtQuick.Window 2.2
0010 import QtQuick.Controls 2.15
0011 import QtQml.Models 2.2
0012 import QtQuick.Layouts 1.2
0013 import Qt5Compat.GraphicalEffects
0014 import org.kde.kirigami 2.12 as Kirigami
0015 import org.kde.elisa 1.0
0016 
0017 Kirigami.Page {
0018     id: topItem
0019 
0020     property int databaseId: 0
0021     property var trackType
0022     property string songTitle: ""
0023     property string albumName: ""
0024     property string artistName: ""
0025     property url albumArtUrl: ""
0026     property url fileUrl: ""
0027     property int albumId
0028     property string albumArtist: ""
0029 
0030     signal openArtist()
0031     signal openAlbum()
0032 
0033     readonly property bool nothingPlaying: albumName.length === 0
0034                                            && artistName.length === 0
0035                                            && albumArtUrl.toString().length === 0
0036                                            && songTitle.length === 0
0037                                            && fileUrl.toString().length === 0
0038 
0039     title: i18nc("@title:window Title of the context view related to the currently playing track", "Now Playing")
0040     padding: 0
0041 
0042     property bool isWidescreen: mainWindow.width >= elisaTheme.viewSelectorSmallSizeThreshold
0043 
0044     onAlbumArtUrlChanged: {
0045         background.loadImage();
0046     }
0047 
0048     TrackContextMetaDataModel {
0049         id: metaDataModel
0050         onLyricsChanged: lyricsModel.setLyric(lyrics)
0051         manager: ElisaApplication.musicManager
0052     }
0053 
0054     // Header with title and actions
0055     globalToolBarStyle: Kirigami.ApplicationHeaderStyle.None
0056     header: ToolBar {
0057         implicitHeight: Math.round(Kirigami.Units.gridUnit * 2.5)
0058 
0059         // Override color to use standard window colors, not header colors
0060         // TODO: remove this if the HeaderBar component is ever removed or moved
0061         // to the bottom of the window such that this toolbar touches the window
0062         // titlebar
0063         Kirigami.Theme.colorSet: Kirigami.Theme.Window
0064 
0065         RowLayout {
0066             anchors.fill: parent
0067             spacing: Kirigami.Units.smallSpacing
0068 
0069             FlatButtonWithToolTip {
0070                 id: showSidebarButton
0071                 objectName: 'showSidebarButton'
0072                 visible: Kirigami.Settings.isMobile
0073                 text: i18nc("@action:button", "Open sidebar")
0074                 icon.name: "open-menu-symbolic"
0075                 onClicked: mainWindow.globalDrawer.open()
0076             }
0077 
0078             Kirigami.Heading {
0079                 Layout.fillWidth: true
0080                 Layout.leftMargin: Kirigami.Units.largeSpacing
0081                 Layout.alignment: Qt.AlignVCenter
0082 
0083                 text: topItem.title
0084             }
0085             // Invisible; this exists purely to make the toolbar height match that
0086             // of the adjacent one
0087             ToolButton {
0088                 icon.name: "edit-paste"
0089                 opacity: 0
0090                 focusPolicy: Qt.NoFocus
0091             }
0092 
0093             ButtonGroup {
0094                 id: nowPlayingButtons
0095                 onCheckedButtonChanged: {
0096                     persistentSettings.nowPlayingPreferLyric = nowPlayingButtons.checkedButton === showLyricButton
0097                 }
0098             }
0099             FlatButtonWithToolTip {
0100                 id: showMetaDataButton
0101                 ButtonGroup.group: nowPlayingButtons
0102 
0103                 readonly property alias item: allMetaDataScroll
0104 
0105                 checkable: true
0106                 checked: !persistentSettings.nowPlayingPreferLyric
0107                 display: topItem.isWidescreen ? AbstractButton.TextBesideIcon : AbstractButton.IconOnly
0108                 icon.name: "documentinfo"
0109                 text: i18nc("@option:radio One of the 'now playing' views", "Metadata")
0110                 visible: !contentLayout.wideMode
0111             }
0112             FlatButtonWithToolTip {
0113                 id: showLyricButton
0114                 ButtonGroup.group: nowPlayingButtons
0115 
0116                 checkable: true
0117                 checked: persistentSettings.nowPlayingPreferLyric
0118                 display: topItem.isWidescreen ? AbstractButton.TextBesideIcon : AbstractButton.IconOnly
0119                 icon.name: "view-media-lyrics"
0120                 text: i18nc("@option:radio One of the 'now playing' views", "Lyrics")
0121                 visible: !contentLayout.wideMode
0122             }
0123 
0124             FlatButtonWithToolTip {
0125                 id: showPlaylistButton
0126                 visible: Kirigami.Settings.isMobile
0127                 text: i18nc("@action:button", "Show Playlist")
0128                 icon.name: "view-media-playlist"
0129                 display: topItem.isWidescreen ? AbstractButton.TextBesideIcon : AbstractButton.IconOnly
0130                 onClicked: {
0131                     if (topItem.isWidescreen) {
0132                         contentView.showPlaylist = !contentView.showPlaylist;
0133                     } else {
0134                         playlistDrawer.open();
0135                     }
0136                 }
0137             }
0138         }
0139     }
0140 
0141     Item {
0142         anchors.fill: parent
0143 
0144         // Blurred album art background
0145         StackView {
0146             id: background
0147             anchors.fill: parent
0148 
0149             readonly property bool active: ElisaApplication.showNowPlayingBackground && !topItem.nothingPlaying
0150             property Item pendingImage
0151             property bool doesSkipAnimation: true
0152 
0153             layer.enabled: true
0154             opacity: 0.2
0155             layer.effect: FastBlur {
0156                 radius: 40
0157             }
0158 
0159             replaceEnter: Transition {
0160                 OpacityAnimator {
0161                     id: replaceEnterOpacityAnimator
0162                     from: 0
0163                     to: 1
0164                     // 1 is HACK for https://bugreports.qt.io/browse/QTBUG-106797 to avoid flickering
0165                     duration: background.doesSkipAnimation ? 1 : Kirigami.Units.longDuration
0166                 }
0167             }
0168             // Keep the old image around till the new one is fully faded in
0169             // If we fade both at the same time you can see the background behind glimpse through
0170             replaceExit: Transition {
0171                 PauseAnimation {
0172                     duration: replaceEnterOpacityAnimator.duration
0173                 }
0174             }
0175 
0176             onActiveChanged: loadImage()
0177 
0178             function loadImage() {
0179                 if (pendingImage) {
0180                     pendingImage.statusChanged.disconnect(replaceWhenLoaded);
0181                     pendingImage.destroy();
0182                     pendingImage = null;
0183                 }
0184 
0185                 if (!active) {
0186                     clear();
0187                     return;
0188                 }
0189 
0190                 doesSkipAnimation = currentItem == undefined;
0191                 pendingImage = backgroundComponent.createObject(background, {
0192                     "source": topItem.albumArtUrl.toString() === "" ? Qt.resolvedUrl(elisaTheme.defaultAlbumImage) : topItem.albumArtUrl,
0193                     "opacity": 0,
0194                 });
0195 
0196                 if (pendingImage.status === Image.Loading) {
0197                     pendingImage.statusChanged.connect(background.replaceWhenLoaded);
0198                 } else {
0199                     background.replaceWhenLoaded();
0200                 }
0201             }
0202 
0203             function replaceWhenLoaded() {
0204                 pendingImage.statusChanged.disconnect(replaceWhenLoaded);
0205                 replace(pendingImage, {}, StackView.Transition);
0206                 pendingImage = null;
0207             }
0208 
0209             Component.onCompleted: {
0210                 loadImage();
0211             }
0212         }
0213 
0214         Component {
0215             id: backgroundComponent
0216 
0217             Image {
0218                 asynchronous: true
0219                 fillMode: Image.PreserveAspectCrop
0220 
0221                 // HACK: set sourceSize to a fixed value to prevent background flickering (BUG431607)
0222                 onStatusChanged: {
0223                     if (status === Image.Ready && (sourceSize.width > Kirigami.Units.gridUnit * 50 || sourceSize.height > Kirigami.Units.gridUnit * 50)) {
0224                         sourceSize = Qt.size(Kirigami.Units.gridUnit * 50, Kirigami.Units.gridUnit * 50);
0225                     }
0226                 }
0227 
0228                 StackView.onRemoved: {
0229                     destroy();
0230                 }
0231             }
0232         }
0233 
0234         RowLayout {
0235             id: contentLayout
0236 
0237             property bool wideMode: allMetaDataLoader.width <= width * 0.5
0238                                     && allMetaDataLoader.height <= height
0239 
0240             anchors.fill: parent
0241             visible: !topItem.nothingPlaying
0242 
0243             spacing:  0
0244 
0245             // Metadata
0246             ScrollView {
0247                 id: allMetaDataScroll
0248 
0249                 implicitWidth: {
0250                     if (contentLayout.wideMode) {
0251                         return contentLayout.width * 0.5
0252                     } else {
0253                         return showMetaDataButton.checked ? contentLayout.width : 0
0254                     }
0255                 }
0256 
0257                 implicitHeight: Math.min(allMetaDataLoader.height, parent.height)
0258 
0259                 contentWidth: availableWidth
0260                 contentHeight: allMetaDataLoader.height
0261 
0262                 // HACK: workaround for https://bugreports.qt.io/browse/QTBUG-83890
0263                 ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
0264 
0265                 Loader {
0266                     id: allMetaDataLoader
0267 
0268                     sourceComponent: Kirigami.FormLayout {
0269                         id: allMetaData
0270                         property real margins: Kirigami.Units.largeSpacing + allMetaDataScroll.ScrollBar.vertical.width
0271                         width: (implicitWidth + margins <= contentLayout.width * 0.5 ? contentLayout.width * 0.5 : contentLayout.width) - margins
0272                         x: wideMode? (allMetaDataScroll.width - width) * 0.5 : Kirigami.Units.largeSpacing
0273 
0274                         Repeater {
0275                             id: trackData
0276                             model: metaDataModel
0277 
0278                             delegate: Item {
0279                                 Kirigami.FormData.label: "<b>" + model.name + ":</b>"
0280                                 implicitWidth: childrenRect.width
0281                                 implicitHeight: childrenRect.height
0282 
0283                                 MediaTrackMetadataDelegate {
0284                                     maximumWidth: contentLayout.width - allMetaData.margins
0285                                     index: model.index
0286                                     name: model.name
0287                                     display: model.display
0288                                     type: model.type
0289                                     readOnly: true
0290                                     url: topItem.fileUrl
0291                                 }
0292                             }
0293                         }
0294                     }
0295 
0296                     // We need unload Kirigami.FormLayout and recreate it
0297                     // to avoid lots of warnings in the terminal
0298                     Timer {
0299                         id: resetTimer
0300                         interval: 0
0301                         onTriggered: {
0302                             allMetaDataLoader.active = true
0303                         }
0304                     }
0305                     Connections {
0306                         target: metaDataModel
0307                         function onModelAboutToBeReset() {
0308                             allMetaDataLoader.active = false
0309                         }
0310                         function onModelReset() {
0311                             resetTimer.restart()
0312                         }
0313                     }
0314                 }
0315             }
0316 
0317             // Lyrics
0318             ScrollView {
0319                 id: lyricScroll
0320                 implicitWidth: {
0321                     if (contentLayout.wideMode) {
0322                         return contentLayout.width * 0.5
0323                     } else {
0324                         return showLyricButton.checked ? contentLayout.width : 0
0325                     }
0326                 }
0327                 implicitHeight: Math.min(lyricItem.height, parent.height)
0328 
0329                 contentWidth: availableWidth
0330                 contentHeight: lyricItem.height
0331 
0332                 // HACK: workaround for https://bugreports.qt.io/browse/QTBUG-83890
0333                 ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
0334                 PropertyAnimation {
0335                     id: lyricScrollAnimation
0336 
0337                     // the target is a flickable
0338                     target: lyricScroll.contentItem
0339                     property: "contentY"
0340                     onToChanged: restart()
0341                 }
0342 
0343                 Item {
0344                     id: lyricItem
0345                     property real margins: Kirigami.Units.largeSpacing + lyricScroll.ScrollBar.vertical.width
0346                     width: lyricScroll.width - margins
0347                     height: lyricsView.count === 0 ? lyricPlaceholder.height : lyricsView.height
0348                     x: Kirigami.Units.largeSpacing
0349 
0350                     ListView {
0351                         id: lyricsView
0352                         height: contentHeight
0353                         width: parent.width
0354                         model: lyricsModel
0355                         delegate: Label {
0356                             text: lyric
0357                             width: lyricItem.width
0358                             wrapMode: Text.WordWrap
0359                             font.bold: ListView.isCurrentItem
0360                             horizontalAlignment: contentLayout.wideMode? Text.AlignLeft : Text.AlignHCenter
0361                             MouseArea {
0362                                 height: parent.height
0363                                 width: Math.min(parent.width, parent.contentWidth)
0364                                 x: contentLayout.wideMode ? 0 : (parent.width - width) / 2
0365                                 enabled: lyricsModel.isLRC
0366                                 cursorShape: enabled ? Qt.PointingHandCursor : undefined
0367                                 onClicked: {
0368                                     ElisaApplication.audioPlayer.position = timestamp;
0369                                 }
0370                             }
0371                         }
0372                         currentIndex: lyricsModel.highlightedIndex
0373                         onCurrentIndexChanged: {
0374                             if (currentIndex === -1)
0375                                 return
0376 
0377                             // center aligned
0378                             var toPos = Math.round(currentItem.y + currentItem.height * 0.5 - lyricScroll.height * 0.5)
0379                             // make sure the first and the last lines are always
0380                             // positioned at the beginning and the end of the view
0381 
0382                             toPos = Math.max(toPos, 0)
0383                             toPos = Math.min(toPos, contentHeight - lyricScroll.height)
0384                             lyricScrollAnimation.to = toPos
0385 
0386                         }
0387                     }
0388 
0389                     LyricsModel {
0390                         id: lyricsModel
0391                     }
0392                     Connections {
0393                         target: ElisaApplication.audioPlayer
0394                         function onPositionChanged(position) {
0395                             lyricsModel.setPosition(position)
0396                         }
0397                     }
0398 
0399                     Loader {
0400                         id: lyricPlaceholder
0401                         anchors.centerIn: parent
0402                         width: parent.width
0403 
0404                         active: lyricsView.count === 0
0405                         visible: active && status === Loader.Ready
0406 
0407                         sourceComponent: Kirigami.PlaceholderMessage {
0408                             text: i18nc("@info:placeholder", "No lyrics found")
0409                             icon.name: "view-media-lyrics"
0410                         }
0411                     }
0412                 }
0413             }
0414         }
0415 
0416         // "Nothing Playing" message
0417         Loader {
0418             anchors.centerIn: parent
0419             width: parent.width - (Kirigami.Units.largeSpacing * 4)
0420 
0421             active: topItem.nothingPlaying
0422             visible: active && status === Loader.Ready
0423 
0424             sourceComponent: Kirigami.PlaceholderMessage {
0425                 text: i18nc("@info:placeholder", "Nothing playing")
0426                 icon.name: "view-media-track"
0427             }
0428         }
0429     }
0430 
0431     // Footer with file path label
0432     footer: ToolBar {
0433         implicitHeight: Math.round(Kirigami.Units.gridUnit * 2)
0434         visible: !topItem.nothingPlaying
0435 
0436         RowLayout {
0437             anchors.fill: parent
0438             spacing: Kirigami.Units.smallSpacing
0439 
0440             LabelWithToolTip {
0441                 id: fileUrlLabel
0442                 text: metaDataModel.fileUrl
0443                 elide: Text.ElideLeft
0444                 Layout.fillWidth: true
0445             }
0446 
0447             Kirigami.ActionToolBar {
0448                 // because fillWidth is true by default
0449                 Layout.fillWidth: false
0450 
0451                 // when there is not enough space, show the button in the compact mode
0452                 // then the file url will be elided if needed
0453                 Layout.preferredWidth: parent.width > fileUrlLabel.implicitWidth + spacing + maximumContentWidth ? maximumContentWidth : Kirigami.Units.gridUnit * 2
0454 
0455                 Layout.fillHeight: true
0456 
0457                 actions: [
0458                     Kirigami.Action {
0459                         text: i18nc("@action:button", "Show In Folder")
0460                         icon.name: 'document-open-folder'
0461                         visible: metaDataModel.fileUrl.toString() !== "" && !metaDataModel.fileUrl.toString().startsWith("http") && !metaDataModel.fileUrl.toString().startsWith("rtsp")
0462                         onTriggered: {
0463                             ElisaApplication.showInFolder(metaDataModel.fileUrl)
0464                         }
0465                     }
0466                 ]
0467             }
0468         }
0469     }
0470 
0471     onFileUrlChanged: {
0472         if (ElisaApplication.musicManager && trackType !== undefined && fileUrl.toString().length !== 0) {
0473             if (databaseId !== 0) {
0474                 metaDataModel.initializeByIdAndUrl(trackType, databaseId, fileUrl)
0475             } else {
0476                 metaDataModel.initializeByUrl(trackType, fileUrl)
0477             }
0478         }
0479     }
0480 
0481     onTrackTypeChanged: {
0482         if (ElisaApplication.musicManager && trackType !== undefined && fileUrl.toString().length !== 0) {
0483             if (databaseId !== 0) {
0484                 metaDataModel.initializeByIdAndUrl(trackType, databaseId, fileUrl)
0485             } else {
0486                 metaDataModel.initializeByUrl(trackType, fileUrl)
0487             }
0488         }
0489     }
0490 
0491     Connections {
0492         target: ElisaApplication
0493 
0494         function onMusicManagerChanged() {
0495             if (ElisaApplication.musicManager && trackType !== undefined && databaseId !== 0) {
0496                 metaDataModel.initializeByIdAndUrl(trackType, databaseId, fileUrl)
0497             }
0498         }
0499     }
0500 
0501     Component.onCompleted: {
0502         if (ElisaApplication.musicManager && trackType !== undefined) {
0503             if (databaseId !== 0) {
0504                 metaDataModel.initializeByIdAndUrl(trackType, databaseId, fileUrl)
0505             } else {
0506                 metaDataModel.initializeByUrl(trackType, fileUrl)
0507             }
0508         }
0509     }
0510 }