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