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 alias entry: entry
0191                         property bool nextDelegateHasSection
0192 
0193                         width: playListView.width
0194                         spacing: 0
0195 
0196                         onFocusChanged: {
0197                             if (focus) {
0198                                 entry.forceActiveFocus()
0199                             }
0200                         }
0201 
0202                         Loader {
0203                             id: albumSection
0204                             active: entry.sectionVisible
0205                             visible: active
0206                             Layout.fillWidth: true
0207                             sourceComponent: BasicPlayListAlbumHeader {
0208                                 headerData: JSON.parse(playListDelegate.ListView.section)
0209                             }
0210                         }
0211 
0212                         // entry's placeholder
0213                         // otherwise the layout is broken while dragging
0214                         FocusScope {
0215                             implicitWidth: entry.width
0216                             // the height must be set because entry's parent
0217                             // will be changed while dragging
0218                             implicitHeight: entry.height
0219 
0220                             // because this is a FocusScope, clicking on any button inside PlayListEntry
0221                             // will change this activeFocus
0222                             onActiveFocusChanged: {
0223                                 if (activeFocus) {
0224                                     playListView.currentIndex = model.index
0225                                     entry.forceActiveFocus()
0226                                 }
0227                             }
0228 
0229                             // ListItemDragHandle requires listItem
0230                             // to be a child of delegate
0231                             PlayListEntry {
0232                                 id: entry
0233 
0234                                 width: playListView.width
0235 
0236                                 index: model.index
0237                                 isSelected: playListView.currentIndex === index
0238 
0239                                 databaseId: model.databaseId ? model.databaseId : 0
0240                                 entryType: model.entryType ? model.entryType : ElisaUtils.Unknown
0241                                 title: model.title ? model.title : ''
0242                                 artist: model.artist ? model.artist : ''
0243                                 album: model.album ? model.album : ''
0244                                 albumArtist: model.albumArtist ? model.albumArtist : ''
0245                                 duration: model.duration ? model.duration : ''
0246                                 fileName: model.trackResource ? model.trackResource : ''
0247                                 imageUrl: model.imageUrl ? model.imageUrl : ''
0248                                 trackNumber: model.trackNumber ? model.trackNumber : -1
0249                                 discNumber: model.discNumber ? model.discNumber : -1
0250                                 rating: model.rating ? model.rating : 0
0251                                 isSingleDiscAlbum: model.isSingleDiscAlbum !== undefined ? model.isSingleDiscAlbum : true
0252                                 isValid: model.isValid
0253                                 isPlaying: model.isPlaying
0254                                 metadataModifiableRole: model && model.metadataModifiableRole ? model.metadataModifiableRole : false
0255                                 listView: playListView
0256                                 listDelegate: playListDelegate
0257                                 showDragHandle: playListView.count > 1
0258                             }
0259                         }
0260                     }
0261 
0262                     onCountChanged: if (count === 0) {
0263                         currentIndex = -1;
0264                     }
0265 
0266                     Loader {
0267                         anchors.centerIn: parent
0268                         width: parent.width - (Kirigami.Units.largeSpacing * 4)
0269 
0270                         active: playListView.count === 0
0271                         visible: active && status === Loader.Ready
0272 
0273                         sourceComponent: Kirigami.PlaceholderMessage {
0274                             icon.name: "view-media-playlist"
0275                             text: i18nc("@info:placeholder", "Playlist is empty")
0276                             explanation: i18nc("@info:usagetip", "Add some songs to get started. You can browse your music using the views on the left.")
0277                         }
0278                     }
0279 
0280                     /* currently disabled animations due to display corruption
0281                     because of https://bugreports.qt.io/browse/QTBUG-49868
0282                     causing https://bugs.kde.org/show_bug.cgi?id=406524
0283                     and https://bugs.kde.org/show_bug.cgi?id=398093
0284                     add: Transition {
0285                         NumberAnimation {
0286                             property: "opacity";
0287                             from: 0;
0288                             to: 1;
0289                             duration: Kirigami.Units.shortDuration }
0290                     }
0291 
0292                     populate: Transition {
0293                         NumberAnimation {
0294                             property: "opacity";
0295                             from: 0;
0296                             to: 1;
0297                             duration: Kirigami.Units.shortDuration }
0298                     }
0299 
0300                     remove: Transition {
0301                         NumberAnimation {
0302                             property: "opacity";
0303                             from: 1.0;
0304                             to: 0;
0305                             duration: Kirigami.Units.shortDuration }
0306                     }
0307 
0308                     displaced: Transition {
0309                         NumberAnimation {
0310                             properties: "x,y";
0311                             duration: Kirigami.Units.shortDuration
0312                             easing.type: Easing.InOutQuad }
0313                     }
0314                     */
0315                 }
0316             }
0317         }
0318 
0319         // ========== mobile listview ==========
0320         Component {
0321             id: mobileListView
0322 
0323             ScrollView {
0324                 property alias list: playListView
0325 
0326                 contentItem: ListView {
0327                     id: playListView
0328 
0329                     reuseItems: true
0330                     model: ElisaApplication.mediaPlayListProxyModel
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                         // ListItemDragHandle requires listItem
0359                         // to be a child of delegate
0360                         MobilePlayListDelegate {
0361                             id: entry
0362                             width: playListView.width
0363 
0364                             index: model ? model.index : 0
0365                             isSelected: playListView.currentIndex === index
0366 
0367                             databaseId: model && model.databaseId ? model.databaseId : 0
0368                             entryType: model && model.entryType ? model.entryType : ElisaUtils.Unknown
0369                             title: model ? model.title || '' : ''
0370                             artist: model ? model.artist || '' : ''
0371                             album: model ? model.album || '' : ''
0372                             albumArtist: model ? model.albumArtist || '' : ''
0373                             duration: model ? model.duration || '' : ''
0374                             fileName: model ? model.trackResource || '' : ''
0375                             imageUrl: model ? model.imageUrl || '' : ''
0376                             trackNumber: model ? model.trackNumber || -1 : -1
0377                             discNumber: model ? model.discNumber || -1 : -1
0378                             rating: model ? model.rating || 0 : 0
0379                             isSingleDiscAlbum: model && model.isSingleDiscAlbum !== undefined ? model.isSingleDiscAlbum : true
0380                             isValid: model && model.isValid
0381                             isPlaying: model ? model.isPlaying : false
0382                             metadataModifiableRole: model && model.metadataModifiableRole ? model.metadataModifiableRole : false
0383                             hideDiscNumber: model && model.isSingleDiscAlbum
0384                             showDragHandle: playListView.count > 1
0385 
0386                             listView: playListView
0387 
0388                             onActiveFocusChanged: {
0389                                 if (activeFocus && playListView.currentIndex !== index) {
0390                                     playListView.currentIndex = index
0391                                 }
0392                             }
0393                         }
0394                     }
0395                 }
0396             }
0397         }
0398 
0399         Loader {
0400             id: playListLoader
0401             Layout.fillWidth: true
0402             Layout.fillHeight: true
0403             sourceComponent: Kirigami.Settings.isMobile ? mobileListView : desktopListView
0404             onLoaded: playListView = item.list
0405         }
0406 
0407         Kirigami.InlineMessage {
0408             id: mobileClearedMessage
0409             Layout.fillWidth: true
0410             visible: false
0411             showCloseButton: true
0412             text: i18nc("@label", "Playlist cleared")
0413 
0414             actions: [
0415                 Kirigami.Action {
0416                     text: i18nc("@action:button", "Undo")
0417                     icon.name: "edit-undo-symbolic"
0418                     onTriggered: ElisaApplication.mediaPlayListProxyModel.undoClearPlayList()
0419                 }
0420             ]
0421         }
0422     }
0423 
0424     footer: ToolBar {
0425         implicitHeight: Math.round(Kirigami.Units.gridUnit * 2)
0426 
0427         leftPadding: mirrored ? undefined : Kirigami.Units.largeSpacing
0428         rightPadding: mirrored ? Kirigami.Units.largeSpacing : undefined
0429 
0430         RowLayout {
0431             anchors.fill: parent
0432             spacing: Kirigami.Units.smallSpacing
0433 
0434             LabelWithToolTip {
0435                 text: {
0436                     if (ElisaApplication.mediaPlayListProxyModel.remainingTracks === -1) {
0437                         return i18ncp("@info:status", "%1 track", "%1 tracks", ElisaApplication.mediaPlayListProxyModel.tracksCount);
0438                     } else {
0439                         if (ElisaApplication.mediaPlayListProxyModel.tracksCount == 1) {
0440                             return i18nc("@info:status", "1 track");
0441                         } else {
0442                             var nTracks = i18ncp("@info:status", "%1 track", "%1 tracks", ElisaApplication.mediaPlayListProxyModel.tracksCount);
0443                             var nRemaining = i18ncp("@info:status number of tracks remaining", "%1 remaining", "%1 remaining", ElisaApplication.mediaPlayListProxyModel.remainingTracks);
0444                             return i18nc("@info:status %1 is the translation of track[s], %2 is the translation of remaining", "%1 (%2)", nTracks, nRemaining);
0445                         }
0446                     }
0447                 }
0448                 elide: Text.ElideLeft
0449             }
0450 
0451             Kirigami.ActionToolBar {
0452                 Layout.fillWidth: true
0453                 Layout.fillHeight: true
0454                 alignment: Qt.AlignRight
0455 
0456                 actions: [
0457                     Kirigami.Action {
0458                         text: i18nc("@action:button", "Show Current")
0459                         icon.name: 'media-track-show-active'
0460                         displayHint: Kirigami.DisplayHint.KeepVisible
0461                         enabled: ElisaApplication.mediaPlayListProxyModel ? ElisaApplication.mediaPlayListProxyModel.tracksCount > 0 : false
0462                         onTriggered: {
0463                             playListView.positionViewAtIndex(ElisaApplication.mediaPlayListProxyModel.currentTrackRow, ListView.Contain)
0464                             playListView.currentIndex = ElisaApplication.mediaPlayListProxyModel.currentTrackRow
0465                             playListView.currentItem.entry.forceActiveFocus()
0466                         }
0467                     },
0468                     Kirigami.Action {
0469                         text: i18nc("@action:button Remove all tracks from play list", "Clear All")
0470                         icon.name: 'edit-clear-all'
0471                         displayHint: Kirigami.DisplayHint.KeepVisible
0472                         enabled: ElisaApplication.mediaPlayListProxyModel ? ElisaApplication.mediaPlayListProxyModel.tracksCount > 0 : false
0473                         onTriggered: ElisaApplication.mediaPlayListProxyModel.clearPlayList()
0474                     }
0475                 ]
0476             }
0477         }
0478     }
0479 }
0480