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             }
0091 
0092             ButtonGroup {
0093                 id: nowPlayingButtons
0094                 onCheckedButtonChanged: {
0095                     persistentSettings.nowPlayingPreferLyric = nowPlayingButtons.checkedButton === showLyricButton
0096                 }
0097             }
0098             FlatButtonWithToolTip {
0099                 id: showMetaDataButton
0100                 ButtonGroup.group: nowPlayingButtons
0101 
0102                 readonly property alias item: allMetaDataScroll
0103 
0104                 checkable: true
0105                 checked: !persistentSettings.nowPlayingPreferLyric
0106                 display: topItem.isWidescreen ? AbstractButton.TextBesideIcon : AbstractButton.IconOnly
0107                 icon.name: "documentinfo"
0108                 text: i18nc("@option:radio One of the 'now playing' views", "Metadata")
0109                 visible: !contentLayout.wideMode
0110             }
0111             FlatButtonWithToolTip {
0112                 id: showLyricButton
0113                 ButtonGroup.group: nowPlayingButtons
0114 
0115                 checkable: true
0116                 checked: persistentSettings.nowPlayingPreferLyric
0117                 display: topItem.isWidescreen ? AbstractButton.TextBesideIcon : AbstractButton.IconOnly
0118                 icon.name: "view-media-lyrics"
0119                 text: i18nc("@option:radio One of the 'now playing' views", "Lyrics")
0120                 visible: !contentLayout.wideMode
0121             }
0122 
0123             FlatButtonWithToolTip {
0124                 id: showPlaylistButton
0125                 visible: Kirigami.Settings.isMobile
0126                 text: i18nc("@action:button", "Show Playlist")
0127                 icon.name: "view-media-playlist"
0128                 display: topItem.isWidescreen ? AbstractButton.TextBesideIcon : AbstractButton.IconOnly
0129                 onClicked: {
0130                     if (topItem.isWidescreen) {
0131                         contentView.showPlaylist = !contentView.showPlaylist;
0132                     } else {
0133                         playlistDrawer.open();
0134                     }
0135                 }
0136             }
0137         }
0138     }
0139 
0140     Item {
0141         anchors.fill: parent
0142 
0143         // Blurred album art background
0144         StackView {
0145             id: background
0146             anchors.fill: parent
0147 
0148             readonly property bool active: ElisaApplication.showNowPlayingBackground && !topItem.nothingPlaying
0149             property Item pendingImage
0150             property bool doesSkipAnimation: true
0151 
0152             layer.enabled: true
0153             opacity: 0.2
0154             layer.effect: FastBlur {
0155                 radius: 40
0156             }
0157 
0158             replaceEnter: Transition {
0159                 OpacityAnimator {
0160                     id: replaceEnterOpacityAnimator
0161                     from: 0
0162                     to: 1
0163                     // 1 is HACK for https://bugreports.qt.io/browse/QTBUG-106797 to avoid flickering
0164                     duration: background.doesSkipAnimation ? 1 : Kirigami.Units.longDuration
0165                 }
0166             }
0167             // Keep the old image around till the new one is fully faded in
0168             // If we fade both at the same time you can see the background behind glimpse through
0169             replaceExit: Transition {
0170                 PauseAnimation {
0171                     duration: replaceEnterOpacityAnimator.duration
0172                 }
0173             }
0174 
0175             onActiveChanged: loadImage()
0176 
0177             function loadImage() {
0178                 if (pendingImage) {
0179                     pendingImage.statusChanged.disconnect(replaceWhenLoaded);
0180                     pendingImage.destroy();
0181                     pendingImage = null;
0182                 }
0183 
0184                 if (!active) {
0185                     clear();
0186                     return;
0187                 }
0188 
0189                 doesSkipAnimation = currentItem == undefined;
0190                 pendingImage = backgroundComponent.createObject(background, {
0191                     "source": topItem.albumArtUrl.toString() === "" ? Qt.resolvedUrl(elisaTheme.defaultAlbumImage) : topItem.albumArtUrl,
0192                     "opacity": 0,
0193                 });
0194 
0195                 if (pendingImage.status === Image.Loading) {
0196                     pendingImage.statusChanged.connect(background.replaceWhenLoaded);
0197                 } else {
0198                     background.replaceWhenLoaded();
0199                 }
0200             }
0201 
0202             function replaceWhenLoaded() {
0203                 pendingImage.statusChanged.disconnect(replaceWhenLoaded);
0204                 replace(pendingImage, {}, StackView.Transition);
0205                 pendingImage = null;
0206             }
0207 
0208             Component.onCompleted: {
0209                 loadImage();
0210             }
0211         }
0212 
0213         Component {
0214             id: backgroundComponent
0215 
0216             Image {
0217                 asynchronous: true
0218                 fillMode: Image.PreserveAspectCrop
0219 
0220                 // HACK: set sourceSize to a fixed value to prevent background flickering (BUG431607)
0221                 onStatusChanged: {
0222                     if (status === Image.Ready && (sourceSize.width > Kirigami.Units.gridUnit * 50 || sourceSize.height > Kirigami.Units.gridUnit * 50)) {
0223                         sourceSize = Qt.size(Kirigami.Units.gridUnit * 50, Kirigami.Units.gridUnit * 50);
0224                     }
0225                 }
0226 
0227                 StackView.onRemoved: {
0228                     destroy();
0229                 }
0230             }
0231         }
0232 
0233         RowLayout {
0234             id: contentLayout
0235 
0236             property bool wideMode: allMetaDataLoader.width <= width * 0.5
0237                                     && allMetaDataLoader.height <= height
0238 
0239             anchors.fill: parent
0240             visible: !topItem.nothingPlaying
0241 
0242             spacing:  0
0243 
0244             // Metadata
0245             ScrollView {
0246                 id: allMetaDataScroll
0247 
0248                 implicitWidth: {
0249                     if (contentLayout.wideMode) {
0250                         return contentLayout.width * 0.5
0251                     } else {
0252                         return showMetaDataButton.checked ? contentLayout.width : 0
0253                     }
0254                 }
0255 
0256                 implicitHeight: Math.min(allMetaDataLoader.height, parent.height)
0257 
0258                 contentWidth: availableWidth
0259                 contentHeight: allMetaDataLoader.height
0260 
0261                 // HACK: workaround for https://bugreports.qt.io/browse/QTBUG-83890
0262                 ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
0263 
0264                 Loader {
0265                     id: allMetaDataLoader
0266 
0267                     sourceComponent: Kirigami.FormLayout {
0268                         id: allMetaData
0269                         property real margins: Kirigami.Units.largeSpacing + allMetaDataScroll.ScrollBar.vertical.width
0270                         width: (implicitWidth + margins <= contentLayout.width * 0.5 ? contentLayout.width * 0.5 : contentLayout.width) - margins
0271                         x: wideMode? (allMetaDataScroll.width - width) * 0.5 : Kirigami.Units.largeSpacing
0272 
0273                         Repeater {
0274                             id: trackData
0275                             model: metaDataModel
0276 
0277                             delegate: Item {
0278                                 Kirigami.FormData.label: "<b>" + model.name + ":</b>"
0279                                 implicitWidth: childrenRect.width
0280                                 implicitHeight: childrenRect.height
0281 
0282                                 MediaTrackMetadataDelegate {
0283                                     maximumWidth: contentLayout.width - allMetaData.margins
0284                                     index: model.index
0285                                     name: model.name
0286                                     display: model.display
0287                                     type: model.type
0288                                     readOnly: true
0289                                     url: topItem.fileUrl
0290                                 }
0291                             }
0292                         }
0293                     }
0294 
0295                     // We need unload Kirigami.FormLayout and recreate it
0296                     // to avoid lots of warnings in the terminal
0297                     Timer {
0298                         id: resetTimer
0299                         interval: 0
0300                         onTriggered: {
0301                             allMetaDataLoader.active = true
0302                         }
0303                     }
0304                     Connections {
0305                         target: metaDataModel
0306                         function onModelAboutToBeReset() {
0307                             allMetaDataLoader.active = false
0308                         }
0309                         function onModelReset() {
0310                             resetTimer.restart()
0311                         }
0312                     }
0313                 }
0314             }
0315 
0316             // Lyrics
0317             ScrollView {
0318                 id: lyricScroll
0319                 implicitWidth: {
0320                     if (contentLayout.wideMode) {
0321                         return contentLayout.width * 0.5
0322                     } else {
0323                         return showLyricButton.checked ? contentLayout.width : 0
0324                     }
0325                 }
0326                 implicitHeight: Math.min(lyricItem.height, parent.height)
0327 
0328                 contentWidth: availableWidth
0329                 contentHeight: lyricItem.height
0330 
0331                 // HACK: workaround for https://bugreports.qt.io/browse/QTBUG-83890
0332                 ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
0333                 PropertyAnimation {
0334                     id: lyricScrollAnimation
0335 
0336                     // the target is a flickable
0337                     target: lyricScroll.contentItem
0338                     property: "contentY"
0339                     onToChanged: restart()
0340                 }
0341 
0342                 Item {
0343                     id: lyricItem
0344                     property real margins: Kirigami.Units.largeSpacing + lyricScroll.ScrollBar.vertical.width
0345                     width: lyricScroll.width - margins
0346                     height: lyricsView.count === 0 ? lyricPlaceholder.height : lyricsView.height
0347                     x: Kirigami.Units.largeSpacing
0348 
0349                     ListView {
0350                         id: lyricsView
0351                         height: contentHeight
0352                         width: parent.width
0353                         model: lyricsModel
0354                         delegate: Label {
0355                             text: lyric
0356                             width: lyricItem.width
0357                             wrapMode: Text.WordWrap
0358                             font.bold: ListView.isCurrentItem
0359                             horizontalAlignment: contentLayout.wideMode? Text.AlignLeft : Text.AlignHCenter
0360                             MouseArea {
0361                                 height: parent.height
0362                                 width: Math.min(parent.width, parent.contentWidth)
0363                                 x: contentLayout.wideMode ? 0 : (parent.width - width) / 2
0364                                 enabled: lyricsModel.isLRC
0365                                 cursorShape: enabled ? Qt.PointingHandCursor : undefined
0366                                 onClicked: {
0367                                     ElisaApplication.audioPlayer.position = timestamp;
0368                                 }
0369                             }
0370                         }
0371                         currentIndex: lyricsModel.highlightedIndex
0372                         onCurrentIndexChanged: {
0373                             if (currentIndex === -1)
0374                                 return
0375 
0376                             // center aligned
0377                             var toPos = Math.round(currentItem.y + currentItem.height * 0.5 - lyricScroll.height * 0.5)
0378                             // make sure the first and the last lines are always
0379                             // positioned at the beginning and the end of the view
0380 
0381                             toPos = Math.max(toPos, 0)
0382                             toPos = Math.min(toPos, contentHeight - lyricScroll.height)
0383                             lyricScrollAnimation.to = toPos
0384 
0385                         }
0386                     }
0387 
0388                     LyricsModel {
0389                         id: lyricsModel
0390                     }
0391                     Connections {
0392                         target: ElisaApplication.audioPlayer
0393                         function onPositionChanged(position) {
0394                             lyricsModel.setPosition(position)
0395                         }
0396                     }
0397 
0398                     Loader {
0399                         id: lyricPlaceholder
0400                         anchors.centerIn: parent
0401                         width: parent.width
0402 
0403                         active: lyricsView.count === 0
0404                         visible: active && status === Loader.Ready
0405 
0406                         sourceComponent: Kirigami.PlaceholderMessage {
0407                             text: i18nc("@info:placeholder", "No lyrics found")
0408                             icon.name: "view-media-lyrics"
0409                         }
0410                     }
0411                 }
0412             }
0413         }
0414 
0415         // "Nothing Playing" message
0416         Loader {
0417             anchors.centerIn: parent
0418             width: parent.width - (Kirigami.Units.largeSpacing * 4)
0419 
0420             active: topItem.nothingPlaying
0421             visible: active && status === Loader.Ready
0422 
0423             sourceComponent: Kirigami.PlaceholderMessage {
0424                 text: i18nc("@info:placeholder", "Nothing playing")
0425                 icon.name: "view-media-track"
0426             }
0427         }
0428     }
0429 
0430     // Footer with file path label
0431     footer: ToolBar {
0432         implicitHeight: Math.round(Kirigami.Units.gridUnit * 2)
0433         visible: !topItem.nothingPlaying
0434 
0435         RowLayout {
0436             anchors.fill: parent
0437             spacing: Kirigami.Units.smallSpacing
0438 
0439             LabelWithToolTip {
0440                 id: fileUrlLabel
0441                 text: metaDataModel.fileUrl
0442                 elide: Text.ElideLeft
0443                 Layout.fillWidth: true
0444             }
0445 
0446             Kirigami.ActionToolBar {
0447                 // because fillWidth is true by default
0448                 Layout.fillWidth: false
0449 
0450                 // when there is not enough space, show the button in the compact mode
0451                 // then the file url will be elided if needed
0452                 Layout.preferredWidth: parent.width > fileUrlLabel.implicitWidth + spacing + maximumContentWidth ? maximumContentWidth : Kirigami.Units.gridUnit * 2
0453 
0454                 Layout.fillHeight: true
0455 
0456                 actions: [
0457                     Kirigami.Action {
0458                         text: i18nc("@action:button", "Show In Folder")
0459                         icon.name: 'document-open-folder'
0460                         visible: metaDataModel.fileUrl.toString() !== "" && !metaDataModel.fileUrl.toString().startsWith("http") && !metaDataModel.fileUrl.toString().startsWith("rtsp")
0461                         onTriggered: {
0462                             ElisaApplication.showInFolder(metaDataModel.fileUrl)
0463                         }
0464                     }
0465                 ]
0466             }
0467         }
0468     }
0469 
0470     onFileUrlChanged: {
0471         if (ElisaApplication.musicManager && trackType !== undefined && fileUrl.toString().length !== 0) {
0472             if (databaseId !== 0) {
0473                 metaDataModel.initializeByIdAndUrl(trackType, databaseId, fileUrl)
0474             } else {
0475                 metaDataModel.initializeByUrl(trackType, fileUrl)
0476             }
0477         }
0478     }
0479 
0480     onTrackTypeChanged: {
0481         if (ElisaApplication.musicManager && trackType !== undefined && fileUrl.toString().length !== 0) {
0482             if (databaseId !== 0) {
0483                 metaDataModel.initializeByIdAndUrl(trackType, databaseId, fileUrl)
0484             } else {
0485                 metaDataModel.initializeByUrl(trackType, fileUrl)
0486             }
0487         }
0488     }
0489 
0490     Connections {
0491         target: ElisaApplication
0492 
0493         function onMusicManagerChanged() {
0494             if (ElisaApplication.musicManager && trackType !== undefined && databaseId !== 0) {
0495                 metaDataModel.initializeByIdAndUrl(trackType, databaseId, fileUrl)
0496             }
0497         }
0498     }
0499 
0500     Component.onCompleted: {
0501         if (ElisaApplication.musicManager && trackType !== undefined) {
0502             if (databaseId !== 0) {
0503                 metaDataModel.initializeByIdAndUrl(trackType, databaseId, fileUrl)
0504             } else {
0505                 metaDataModel.initializeByUrl(trackType, fileUrl)
0506             }
0507         }
0508     }
0509 }