Warning, /multimedia/elisa/src/qml/MediaPlayListView.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
0009 import QtQuick.Controls 2.3
0010 import QtQuick.Layouts 1.1
0011 import QtQuick.Window 2.2
0012 import QtQml.Models 2.1
0013 import org.kde.kirigami 2.15 as Kirigami
0014 import org.kde.elisa 1.0
0015 
0016 import "mobile"
0017 
0018 // Not using ScrollablePage because we don't need any of the refresh features
0019 // that it provides
0020 Kirigami.Page {
0021     id: topItem
0022 
0023     // set by the respective mobile/desktop view
0024     property var playListNotification
0025     property var playListView
0026 
0027     title: i18nc("@title:window Title of the view of the playlist; keep this string as short as possible because horizontal space is quite scarce", "Playlist")
0028     padding: 0
0029 
0030     // Use view colors so the background is white
0031     Kirigami.Theme.inherit: false
0032     Kirigami.Theme.colorSet: Kirigami.Theme.View
0033 
0034     Accessible.role: Accessible.Pane
0035     Accessible.name: topItem.title
0036 
0037     // Header with title and actions
0038     globalToolBarStyle: Kirigami.ApplicationHeaderStyle.None
0039     header: ToolBar {
0040         implicitHeight: Math.round(Kirigami.Units.gridUnit * 2.5)
0041 
0042         leftPadding: mirrored ? undefined : Kirigami.Units.largeSpacing
0043         rightPadding: mirrored ? Kirigami.Units.largeSpacing : undefined
0044 
0045         // Override color to use standard window colors, not header colors
0046         // TODO: remove this if the HeaderBar component is ever removed or moved
0047         // to the bottom of the window such that this toolbar touches the window
0048         // titlebar
0049         Kirigami.Theme.colorSet: Kirigami.Theme.Window
0050 
0051         RowLayout {
0052             anchors.fill: parent
0053             spacing: Kirigami.Units.smallSpacing
0054 
0055             Kirigami.Heading {
0056                 text: topItem.title
0057             }
0058             Kirigami.ActionToolBar {
0059                 Layout.fillWidth: true
0060                 alignment: Qt.AlignRight
0061 
0062                 actions: [
0063                     Kirigami.Action {
0064                         id: savePlaylistButton
0065                         text: i18nc("@action:button Save a playlist file", "Save…")
0066                         icon.name: 'document-save'
0067                         displayHint: Kirigami.DisplayHint.KeepVisible
0068                         enabled: ElisaApplication.mediaPlayListProxyModel ? ElisaApplication.mediaPlayListProxyModel.tracksCount > 0 : false
0069                         onTriggered: {
0070                             mainWindow.fileDialog.savePlaylist()
0071                         }
0072                     },
0073                     Kirigami.Action {
0074                         id: loadPlaylistButton
0075                         text: i18nc("@action:button Load a playlist file", "Load…")
0076                         icon.name: 'document-open'
0077                         displayHint: Kirigami.DisplayHint.KeepVisible
0078                         onTriggered: {
0079                             mainWindow.fileDialog.loadPlaylist()
0080                         }
0081                     }
0082                 ]
0083             }
0084         }
0085     }
0086 
0087     ColumnLayout {
0088         anchors.fill: parent
0089 
0090         Kirigami.InlineMessage {
0091             id: partiallyLoadedMessage
0092             Layout.fillWidth: true
0093             Layout.margins: Kirigami.Units.smallSpacing
0094 
0095             visible: false
0096             showCloseButton: true
0097 
0098             type: Kirigami.MessageType.Warning
0099             text: i18nc("@info", "Failed to load some tracks. Make sure that they have not been removed or renamed.")
0100 
0101             actions: [
0102                 Kirigami.Action {
0103                     id: actionButton
0104                     visible: false
0105                     text: i18nc("@action:button", "Edit Playlist File")
0106                     icon.name: "document-edit"
0107                     onTriggered: {
0108                         ElisaApplication.mediaPlayListProxyModel.openLoadedPlayList()
0109                         partiallyLoadedMessage.visible = false
0110                     }
0111                 }
0112             ]
0113 
0114             onVisibleChanged: {
0115                 if (!visible) {
0116                     ElisaApplication.mediaPlayListProxyModel.resetPartiallyLoaded()
0117                 }
0118             }
0119 
0120             Connections {
0121                 target: ElisaApplication.mediaPlayListProxyModel
0122                 function onPartiallyLoadedChanged() {
0123                     partiallyLoadedMessage.visible = ElisaApplication.mediaPlayListProxyModel.partiallyLoaded
0124                 }
0125                 function onCanOpenLoadedPlaylistChanged() {
0126                     actionButton.visible = ElisaApplication.mediaPlayListProxyModel.canOpenLoadedPlaylist
0127                 }
0128             }
0129         }
0130 
0131         // ========== desktop listview ==========
0132         Component {
0133             id: desktopListView
0134 
0135             ScrollView {
0136                 property alias list: playListView
0137 
0138                 // HACK: workaround for https://bugreports.qt.io/browse/QTBUG-83890
0139                 ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
0140 
0141                 contentItem: ListView {
0142                     id: playListView
0143 
0144                     signal moveItemRequested(int oldIndex, int newIndex)
0145                     property bool draggingEntry: false
0146 
0147                     reuseItems: true
0148                     clip: true
0149                     keyNavigationEnabled: true
0150                     activeFocusOnTab: count > 0
0151 
0152                     currentIndex: -1
0153 
0154                     onActiveFocusChanged: {
0155                         if (activeFocus) {
0156                             // callLater to make sure this is a Tab focusing
0157                             Qt.callLater(function() {
0158                                 // if the currentItem is not visible, focus on the top fully visible item
0159                                 if (!itemIsFullyVisible(currentIndex)) {
0160                                     const topIndex = indexAt(0, contentY)
0161                                     if (topIndex > -1) {
0162                                         playListView.currentIndex = Math.min(itemIsFullyVisible(topIndex) ? topIndex : topIndex + 1, playListView.count - 1)
0163                                         playListView.currentItem.forceActiveFocus()
0164                                     }
0165                                 }
0166                             })
0167                         }
0168                     }
0169 
0170                     function itemIsFullyVisible(index) {
0171                         if (index < 0 || index > count - 1) {
0172                             return false
0173                         }
0174                         const item = itemAtIndex(index)
0175                         // `item === null` can be true if the delegate is not loaded, hence check `item`
0176                         return item && contentY <= item.y && item.y <= (contentY + height)
0177                     }
0178 
0179                     Accessible.role: Accessible.List
0180                     Accessible.name: topItem.title
0181 
0182                     section.property: 'albumSection'
0183                     section.criteria: ViewSection.FullString
0184 
0185                     model: ElisaApplication.mediaPlayListProxyModel
0186 
0187                     delegate: ColumnLayout {
0188                         id: playListDelegate
0189 
0190                         property bool nextDelegateHasSection
0191 
0192                         width: playListView.width
0193                         spacing: 0
0194 
0195                         onFocusChanged: {
0196                             if (focus) {
0197                                 entry.forceActiveFocus()
0198                             }
0199                         }
0200 
0201                         Loader {
0202                             id: albumSection
0203                             active: entry.sectionVisible
0204                             visible: active
0205                             Layout.fillWidth: true
0206                             sourceComponent: BasicPlayListAlbumHeader {
0207                                 headerData: JSON.parse(playListDelegate.ListView.section)
0208                             }
0209                         }
0210 
0211                         // entry's placeholder
0212                         // otherwise the layout is broken while dragging
0213                         FocusScope {
0214                             implicitWidth: entry.width
0215                             // the height must be set because entry's parent
0216                             // will be changed while dragging
0217                             implicitHeight: entry.height
0218 
0219                             // because this is a FocusScope, clicking on any button inside PlayListEntry
0220                             // will change this activeFocus
0221                             onActiveFocusChanged: {
0222                                 if (activeFocus) {
0223                                     playListView.currentIndex = model.index
0224                                     entry.forceActiveFocus()
0225                                 }
0226                             }
0227 
0228                             // ListItemDragHandle requires listItem
0229                             // to be a child of delegate
0230                             PlayListEntry {
0231                                 id: entry
0232 
0233                                 width: playListView.width
0234 
0235                                 index: model.index
0236                                 isSelected: playListView.currentIndex === index
0237 
0238                                 databaseId: model.databaseId ? model.databaseId : 0
0239                                 entryType: model.entryType ? model.entryType : ElisaUtils.Unknown
0240                                 title: model.title ? model.title : ''
0241                                 artist: model.artist ? model.artist : ''
0242                                 album: model.album ? model.album : ''
0243                                 albumArtist: model.albumArtist ? model.albumArtist : ''
0244                                 duration: model.duration ? model.duration : ''
0245                                 fileName: model.trackResource ? model.trackResource : ''
0246                                 imageUrl: model.imageUrl ? model.imageUrl : ''
0247                                 trackNumber: model.trackNumber ? model.trackNumber : -1
0248                                 discNumber: model.discNumber ? model.discNumber : -1
0249                                 rating: model.rating ? model.rating : 0
0250                                 isSingleDiscAlbum: model.isSingleDiscAlbum !== undefined ? model.isSingleDiscAlbum : true
0251                                 isValid: model.isValid
0252                                 isPlaying: model.isPlaying
0253                                 metadataModifiableRole: model && model.metadataModifiableRole ? model.metadataModifiableRole : false
0254                                 listView: playListView
0255                                 listDelegate: playListDelegate
0256                                 showDragHandle: playListView.count > 1
0257                             }
0258                         }
0259                     }
0260 
0261                     onCountChanged: if (count === 0) {
0262                         currentIndex = -1;
0263                     }
0264 
0265                     Loader {
0266                         anchors.centerIn: parent
0267                         width: parent.width - (Kirigami.Units.largeSpacing * 4)
0268 
0269                         active: playListView.count === 0
0270                         visible: active && status === Loader.Ready
0271 
0272                         sourceComponent: Kirigami.PlaceholderMessage {
0273                             icon.name: "view-media-playlist"
0274                             text: i18nc("@info:placeholder", "Playlist is empty")
0275                             explanation: i18nc("@info:usagetip", "Add some songs to get started. You can browse your music using the views on the left.")
0276                         }
0277                     }
0278 
0279                     /* currently disabled animations due to display corruption
0280                     because of https://bugreports.qt.io/browse/QTBUG-49868
0281                     causing https://bugs.kde.org/show_bug.cgi?id=406524
0282                     and https://bugs.kde.org/show_bug.cgi?id=398093
0283                     add: Transition {
0284                         NumberAnimation {
0285                             property: "opacity";
0286                             from: 0;
0287                             to: 1;
0288                             duration: Kirigami.Units.shortDuration }
0289                     }
0290 
0291                     populate: Transition {
0292                         NumberAnimation {
0293                             property: "opacity";
0294                             from: 0;
0295                             to: 1;
0296                             duration: Kirigami.Units.shortDuration }
0297                     }
0298 
0299                     remove: Transition {
0300                         NumberAnimation {
0301                             property: "opacity";
0302                             from: 1.0;
0303                             to: 0;
0304                             duration: Kirigami.Units.shortDuration }
0305                     }
0306 
0307                     displaced: Transition {
0308                         NumberAnimation {
0309                             properties: "x,y";
0310                             duration: Kirigami.Units.shortDuration
0311                             easing.type: Easing.InOutQuad }
0312                     }
0313                     */
0314                 }
0315             }
0316         }
0317 
0318         // ========== mobile listview ==========
0319         Component {
0320             id: mobileListView
0321 
0322             ScrollView {
0323                 property alias list: playListView
0324 
0325                 contentItem: ListView {
0326                     id: playListView
0327 
0328                     reuseItems: true
0329                     model: ElisaApplication.mediaPlayListProxyModel
0330                     activeFocusOnTab: count > 0
0331 
0332                     moveDisplaced: Transition {
0333                         YAnimator {
0334                             duration: Kirigami.Units.longDuration
0335                             easing.type: Easing.InOutQuad
0336                         }
0337                     }
0338 
0339                     Loader {
0340                         anchors.centerIn: parent
0341                         width: parent.width - (Kirigami.Units.largeSpacing * 4)
0342 
0343                         active: ElisaApplication.mediaPlayListProxyModel ? ElisaApplication.mediaPlayListProxyModel.tracksCount === 0 : true
0344                         visible: active && status === Loader.Ready
0345 
0346                         sourceComponent: Kirigami.PlaceholderMessage {
0347                             anchors.centerIn: parent
0348 
0349                             icon.name: "view-media-playlist"
0350                             text: i18nc("@info:placeholder", "Your playlist is empty.")
0351                         }
0352                     }
0353 
0354                     delegate: Item {
0355                         width: entry.width
0356                         height: entry.height
0357 
0358                         onFocusChanged: {
0359                             if (focus) {
0360                                 entry.forceActiveFocus()
0361                             }
0362                         }
0363 
0364                         // ListItemDragHandle requires listItem
0365                         // to be a child of delegate
0366                         MobilePlayListDelegate {
0367                             id: entry
0368                             width: playListView.width
0369 
0370                             index: model ? model.index : 0
0371                             isSelected: playListView.currentIndex === index
0372 
0373                             databaseId: model && model.databaseId ? model.databaseId : 0
0374                             entryType: model && model.entryType ? model.entryType : ElisaUtils.Unknown
0375                             title: model ? model.title || '' : ''
0376                             artist: model ? model.artist || '' : ''
0377                             album: model ? model.album || '' : ''
0378                             albumArtist: model ? model.albumArtist || '' : ''
0379                             duration: model ? model.duration || '' : ''
0380                             fileName: model ? model.trackResource || '' : ''
0381                             imageUrl: model ? model.imageUrl || '' : ''
0382                             trackNumber: model ? model.trackNumber || -1 : -1
0383                             discNumber: model ? model.discNumber || -1 : -1
0384                             rating: model ? model.rating || 0 : 0
0385                             isSingleDiscAlbum: model && model.isSingleDiscAlbum !== undefined ? model.isSingleDiscAlbum : true
0386                             isValid: model && model.isValid
0387                             isPlaying: model ? model.isPlaying : false
0388                             metadataModifiableRole: model && model.metadataModifiableRole ? model.metadataModifiableRole : false
0389                             hideDiscNumber: model && model.isSingleDiscAlbum
0390                             showDragHandle: playListView.count > 1
0391 
0392                             listView: playListView
0393 
0394                             onActiveFocusChanged: {
0395                                 if (activeFocus && playListView.currentIndex !== index) {
0396                                     playListView.currentIndex = index
0397                                 }
0398                             }
0399                         }
0400                     }
0401                 }
0402             }
0403         }
0404 
0405         Loader {
0406             id: playListLoader
0407             Layout.fillWidth: true
0408             Layout.fillHeight: true
0409             sourceComponent: Kirigami.Settings.isMobile ? mobileListView : desktopListView
0410             onLoaded: playListView = item.list
0411         }
0412 
0413         Kirigami.InlineMessage {
0414             id: mobileClearedMessage
0415             Layout.fillWidth: true
0416             visible: false
0417             showCloseButton: true
0418             text: i18nc("@label", "Playlist cleared")
0419 
0420             actions: [
0421                 Kirigami.Action {
0422                     text: i18nc("@action:button", "Undo")
0423                     icon.name: "edit-undo-symbolic"
0424                     onTriggered: ElisaApplication.mediaPlayListProxyModel.undoClearPlayList()
0425                 }
0426             ]
0427         }
0428     }
0429 
0430     footer: ToolBar {
0431         implicitHeight: Math.round(Kirigami.Units.gridUnit * 2)
0432 
0433         leftPadding: mirrored ? undefined : Kirigami.Units.largeSpacing
0434         rightPadding: mirrored ? Kirigami.Units.largeSpacing : undefined
0435 
0436         RowLayout {
0437             anchors.fill: parent
0438             spacing: Kirigami.Units.smallSpacing
0439 
0440             LabelWithToolTip {
0441                 text: {
0442                     if (ElisaApplication.mediaPlayListProxyModel.remainingTracks === -1) {
0443                         return i18ncp("@info:status", "%1 track", "%1 tracks", ElisaApplication.mediaPlayListProxyModel.tracksCount);
0444                     } else {
0445                         if (ElisaApplication.mediaPlayListProxyModel.tracksCount == 1) {
0446                             return i18nc("@info:status", "1 track");
0447                         } else {
0448                             var nTracks = i18ncp("@info:status", "%1 track", "%1 tracks", ElisaApplication.mediaPlayListProxyModel.tracksCount);
0449                             var nRemaining = i18ncp("@info:status number of tracks remaining", "%1 remaining", "%1 remaining", ElisaApplication.mediaPlayListProxyModel.remainingTracks);
0450                             return i18nc("@info:status %1 is the translation of track[s], %2 is the translation of remaining", "%1 (%2)", nTracks, nRemaining);
0451                         }
0452                     }
0453                 }
0454                 elide: Text.ElideLeft
0455             }
0456 
0457             Kirigami.ActionToolBar {
0458                 Layout.fillWidth: true
0459                 Layout.fillHeight: true
0460                 alignment: Qt.AlignRight
0461 
0462                 actions: [
0463                     Kirigami.Action {
0464                         text: i18nc("@action:button", "Show Current")
0465                         icon.name: 'media-track-show-active'
0466                         displayHint: Kirigami.DisplayHint.KeepVisible
0467                         enabled: ElisaApplication.mediaPlayListProxyModel ? ElisaApplication.mediaPlayListProxyModel.tracksCount > 0 : false
0468                         onTriggered: {
0469                             playListView.positionViewAtIndex(ElisaApplication.mediaPlayListProxyModel.currentTrackRow, ListView.Contain)
0470                             playListView.currentIndex = ElisaApplication.mediaPlayListProxyModel.currentTrackRow
0471                             playListView.currentItem.forceActiveFocus()
0472                         }
0473                     },
0474                     Kirigami.Action {
0475                         text: i18nc("@action:button Remove all tracks from play list", "Clear All")
0476                         icon.name: 'edit-clear-all'
0477                         displayHint: Kirigami.DisplayHint.KeepVisible
0478                         enabled: ElisaApplication.mediaPlayListProxyModel ? ElisaApplication.mediaPlayListProxyModel.tracksCount > 0 : false
0479                         onTriggered: ElisaApplication.mediaPlayListProxyModel.clearPlayList()
0480                     }
0481                 ]
0482             }
0483         }
0484     }
0485 }
0486