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