MediaPlayListView.qml 21.7 KB
Newer Older
1
/*
Matthieu Gallien's avatar
Matthieu Gallien committed
2
3
4
5
   SPDX-FileCopyrightText: 2016 (c) Matthieu Gallien <matthieu_gallien@yahoo.fr>
   SPDX-FileCopyrightText: 2019 (c) Nate Graham <nate@kde.org>

   SPDX-License-Identifier: LGPL-3.0-or-later
6
7
 */

Devin Lin's avatar
Devin Lin committed
8
import QtQuick 2.15
9
import QtQuick.Controls 2.3
10
11
import QtQuick.Layouts 1.1
import QtQuick.Window 2.2
12
import QtQml.Models 2.1
13
import Qt.labs.platform 1.0 as PlatformDialog
14
import org.kde.kirigami 2.15 as Kirigami
15
import org.kde.elisa 1.0
16

Devin Lin's avatar
Devin Lin committed
17
18
import "mobile"

19
20
21
22
// Not using ScrollablePage because we don't need any of the refresh features
// that it provides
Kirigami.Page {
    id: topItem
23

24
    signal startPlayback()
25
    signal pausePlayback()
26

Devin Lin's avatar
Devin Lin committed
27
28
29
30
    // set by the respective mobile/desktop view
    property var playListNotification
    property var playListView

31
32
33
34
    function hideNotification() {
        playListNotification.visible = false;
    }

35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
    function showPlayListNotification(message, type, action) {
        if (!message) {
            return;
        }

        if (type) {
            playListNotification.type = type;
        } else {
            playListNotification.type = Kirigami.MessageType.Information;
        }

        if (action) {
            playListNotification.actions = action;
        } else {
            playListNotification.actions = [];
        }

        playListNotification.text = message ? message : "";
        playListNotification.visible = true;
    }

56
57
58
59
60
61
62
63
64
65

    title: i18nc("@info Title of the view of the playlist; keep this string as short as possible because horizontal space is quite scarce", "Playlist")
    padding: 0

    // Use view colors so the background is white
    Kirigami.Theme.inherit: false
    Kirigami.Theme.colorSet: Kirigami.Theme.View

    Accessible.role: Accessible.Pane
    Accessible.name: topItem.title
66

Devin Lin's avatar
Devin Lin committed
67
68
69
70
71
72
    Timer {
        id: mobileClearedMessageTimer
        interval: 3000
        onTriggered: mobileClearedMessage.visible = false
    }

73
74
75
76
    Kirigami.Action {
        id: undoAction
        text: i18nc("Undo", "Undo")
        icon.name: "dialog-cancel"
77
        onTriggered: ElisaApplication.mediaPlayListProxyModel.undoClearPlayList()
78
79
80
81
82
83
    }

    Kirigami.Action {
        id: retryLoadAction
        text: i18nc("Retry", "Retry")
        icon.name: "edit-redo"
84
        onTriggered: loadPlaylistButton.clicked()
85
86
87
88
89
90
    }

    Kirigami.Action {
        id: retrySaveAction
        text: i18nc("Retry", "Retry")
        icon.name: "edit-redo"
91
        onTriggered: savePlaylistButton.clicked()
92
93
94
    }

    Connections {
95
        target: ElisaApplication.mediaPlayListProxyModel
96
        function onPlayListLoadFailed() {
97
98
99
100
101
            showPlayListNotification(i18nc("Message when playlist load failed", "Loading failed"), Kirigami.MessageType.Error, retryLoadAction)
        }
    }

    Connections {
102
         target: ElisaApplication.mediaPlayListProxyModel
103
         function onDisplayUndoNotification() {
Devin Lin's avatar
Devin Lin committed
104
105
106
107
108
109
110
             if (Kirigami.Settings.isMobile) {
                 // cleared playlist message
                mobileClearedMessage.visible = true;
                mobileClearedMessageTimer.restart();
            } else {
                showPlayListNotification(i18nc("Playlist cleared", "Playlist cleared"), Kirigami.MessageType.Information, undoAction);
            }
111
112
113
114
         }
    }

    Connections {
115
         target: ElisaApplication.mediaPlayListProxyModel
116
117
118
         function onHideUndoNotification() {
            hideNotification()
         }
119
    }
120

121
122
123
    // TODO: Once we depend on Frameworks 5.80, change this to
    // "Kirigami.ApplicationHeaderStyle.None" and remove the custom header
    globalToolBarStyle: Kirigami.ApplicationHeaderStyle.None
124
125
126
127
128
129
    header: ToolBar {
        // Override color to use standard window colors, not header colors
        // TODO: remove this if the HeaderBar component is ever removed or moved
        // to the bottom of the window such that this toolbar touches the window
        // titlebar
        Kirigami.Theme.colorSet: Kirigami.Theme.Window
Devin Lin's avatar
Devin Lin committed
130
        implicitHeight: Math.round(Kirigami.Units.gridUnit * 2.5)
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184

        RowLayout {
            anchors.fill: parent

            Kirigami.Heading {
                text: topItem.title
            }
            Kirigami.ActionToolBar {
                Layout.fillWidth: true
                alignment: Qt.AlignRight

                actions: [
                    Kirigami.Action {
                        text: i18nc("Show currently played track inside playlist", "Show Current Track")
                        icon.name: 'media-track-show-active'
                        displayHint: Kirigami.DisplayHint.KeepVisible | Kirigami.DisplayHint.IconOnly
                        enabled: ElisaApplication.mediaPlayListProxyModel ? ElisaApplication.mediaPlayListProxyModel.tracksCount > 0 : false
                        onTriggered: {
                            playListView.positionViewAtIndex(ElisaApplication.mediaPlayListProxyModel.currentTrackRow, ListView.Contain)
                            playListView.currentIndex = ElisaApplication.mediaPlayListProxyModel.currentTrackRow
                            playListView.currentItem.forceActiveFocus()
                        }
                    },
                    Kirigami.Action {
                        id: savePlaylistButton
                        text: i18nc("Save a playlist file", "Save Playlist...")
                        icon.name: 'document-save'
                        displayHint: Kirigami.DisplayHint.KeepVisible | Kirigami.DisplayHint.IconOnly
                        enabled: ElisaApplication.mediaPlayListProxyModel ? ElisaApplication.mediaPlayListProxyModel.tracksCount > 0 : false
                        onTriggered: {
                            fileDialog.fileMode = PlatformDialog.FileDialog.SaveFile
                            fileDialog.file = ''
                            fileDialog.open()
                        }
                    },
                    Kirigami.Action {
                        id: loadPlaylistButton
                        text: i18nc("Load a playlist file", "Load Playlist...")
                        icon.name: 'document-open'
                        displayHint: Kirigami.DisplayHint.KeepVisible | Kirigami.DisplayHint.IconOnly
                        onTriggered: {
                            fileDialog.fileMode = PlatformDialog.FileDialog.OpenFile
                            fileDialog.file = ''
                            fileDialog.open()
                        }
                    },
                    Kirigami.Action {
                        text: i18nc("Remove all tracks from play list", "Clear Playlist")
                        icon.name: 'edit-clear-all'
                        displayHint: Kirigami.DisplayHint.KeepVisible | Kirigami.DisplayHint.IconOnly
                        enabled: ElisaApplication.mediaPlayListProxyModel ? ElisaApplication.mediaPlayListProxyModel.tracksCount > 0 : false
                        onTriggered: ElisaApplication.mediaPlayListProxyModel.clearPlayList()
                    }
                ]
185
186
187
188
            }
        }
    }

Devin Lin's avatar
Devin Lin committed
189
    ColumnLayout {
190
191
        anchors.fill: parent

Devin Lin's avatar
Devin Lin committed
192
193
194
195
196
197
198
        // ========== desktop listview ==========
        Component {
            id: desktopListView
            ScrollView {
                property alias list: playListView
                ListView {
                    id: playListView
199

Devin Lin's avatar
Devin Lin committed
200
201
202
203
                    focus: true
                    clip: true
                    keyNavigationEnabled: true
                    activeFocusOnTab: true
204

Devin Lin's avatar
Devin Lin committed
205
                    currentIndex: -1
206

Devin Lin's avatar
Devin Lin committed
207
208
                    Accessible.role: Accessible.List
                    Accessible.name: topItem.title
209

Devin Lin's avatar
Devin Lin committed
210
211
212
213
214
215
216
                    section.property: 'albumSection'
                    section.criteria: ViewSection.FullString
                    section.labelPositioning: ViewSection.InlineLabels
                    section.delegate: BasicPlayListAlbumHeader {
                        headerData: JSON.parse(section)
                        width: playListView.width
                    }
217

Devin Lin's avatar
Devin Lin committed
218
219
220
221
222
223
224
225
226
227
228
                    /* currently disabled animations due to display corruption
                    because of https://bugreports.qt.io/browse/QTBUG-49868
                    causing https://bugs.kde.org/show_bug.cgi?id=406524
                    and https://bugs.kde.org/show_bug.cgi?id=398093
                    add: Transition {
                        NumberAnimation {
                            property: "opacity";
                            from: 0;
                            to: 1;
                            duration: Kirigami.Units.shortDuration }
                    }
229

Devin Lin's avatar
Devin Lin committed
230
231
232
233
234
235
236
                    populate: Transition {
                        NumberAnimation {
                            property: "opacity";
                            from: 0;
                            to: 1;
                            duration: Kirigami.Units.shortDuration }
                    }
237

Devin Lin's avatar
Devin Lin committed
238
239
240
241
242
243
244
                    remove: Transition {
                        NumberAnimation {
                            property: "opacity";
                            from: 1.0;
                            to: 0;
                            duration: Kirigami.Units.shortDuration }
                    }
245

Devin Lin's avatar
Devin Lin committed
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
                    displaced: Transition {
                        NumberAnimation {
                            properties: "x,y";
                            duration: Kirigami.Units.shortDuration
                            easing.type: Easing.InOutQuad }
                    }
                    */

                    model: DelegateModel {
                        model: ElisaApplication.mediaPlayListProxyModel

                        groups: [
                            DelegateModelGroup { name: "selected" }
                        ]

                        delegate: DraggableItem {
                            id: item
                            width: playListView.width
                            placeholderHeight: elisaTheme.dragDropPlaceholderHeight

                            focus: true

                            PlayListEntry {
                                id: entry

                                focus: true

                                width: parent.width

                                index: model.index
                                isAlternateColor: item.DelegateModel.itemsIndex % 2
                                isSelected: playListView.currentIndex === index
                                containsMouse: item.containsMouse

                                databaseId: model.databaseId ? model.databaseId : 0
                                entryType: model.entryType ? model.entryType : ElisaUtils.Unknown
                                title: model.title ? model.title : ''
                                artist: model.artist ? model.artist : ''
                                album: model.album ? model.album : ''
                                albumArtist: model.albumArtist ? model.albumArtist : ''
                                duration: model.duration ? model.duration : ''
                                fileName: model.trackResource ? model.trackResource : ''
                                imageUrl: model.imageUrl ? model.imageUrl : ''
                                trackNumber: model.trackNumber ? model.trackNumber : -1
                                discNumber: model.discNumber ? model.discNumber : -1
                                rating: model.rating ? model.rating : 0
                                isSingleDiscAlbum: model.isSingleDiscAlbum !== undefined ? model.isSingleDiscAlbum : true
                                isValid: model.isValid
                                isPlaying: model.isPlaying
                                metadataModifiableRole: model ? model.metadataModifiableRole : false

                                onStartPlayback: topItem.startPlayback()
                                onPausePlayback: topItem.pausePlayback()
                                onRemoveFromPlaylist: ElisaApplication.mediaPlayListProxyModel.removeRow(trackIndex)
                                onSwitchToTrack: ElisaApplication.mediaPlayListProxyModel.switchTo(trackIndex)

                                onActiveFocusChanged: {
                                    if (activeFocus && playListView.currentIndex !== index) {
                                        playListView.currentIndex = index
                                    }
                                }
                            }
308

Devin Lin's avatar
Devin Lin committed
309
                            draggedItemParent: playListView
310

Devin Lin's avatar
Devin Lin committed
311
312
313
314
                            onClicked: {
                                playListView.currentIndex = index
                                entry.forceActiveFocus()
                            }
315

Devin Lin's avatar
Devin Lin committed
316
317
318
319
320
321
                            onDoubleClicked: {
                                if (model.isValid) {
                                    ElisaApplication.mediaPlayListProxyModel.switchTo(model.index)
                                    topItem.startPlayback()
                                }
                            }
322

Devin Lin's avatar
Devin Lin committed
323
324
                            onMoveItemRequested: {
                                ElisaApplication.mediaPlayListProxyModel.moveRow(from, to);
325
326
                            }
                        }
327
                    }
328

Devin Lin's avatar
Devin Lin committed
329
330
331
                    onCountChanged: if (count === 0) {
                        currentIndex = -1;
                    }
332

Devin Lin's avatar
Devin Lin committed
333
334
335
336
337
                    Kirigami.PlaceholderMessage {
                        anchors.centerIn: parent
                        width: parent.width - (Kirigami.Units.largeSpacing * 4)
                        text: xi18nc("@info", "Your playlist is empty.<nl/><nl/>Add some songs to get started. You can browse your music using the views on the left.")
                        visible: playListView.count === 0
338
                    }
339

Devin Lin's avatar
Devin Lin committed
340
341
342
343
344
345
346
347
348
                    Kirigami.InlineMessage {
                        id: playListNotification
                        Component.onCompleted: topItem.playListNotification = playListNotification

                        anchors {
                            left: parent.left
                            right: parent.right
                            bottom: parent.bottom
                            margins: Kirigami.Units.largeSpacing
349
                        }
Diego Gangl's avatar
Diego Gangl committed
350

Devin Lin's avatar
Devin Lin committed
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
                        type: Kirigami.MessageType.Information
                        showCloseButton: true

                        onVisibleChanged: {
                            if (visible) {
                                autoHideNotificationTimer.start()
                            } else {
                                autoHideNotificationTimer.stop()
                            }
                        }

                        Timer {
                            id: autoHideNotificationTimer
                            interval: 7000
                            onTriggered: playListNotification.visible = false
                        }
367
368
369
                    }
                }
            }
Devin Lin's avatar
Devin Lin committed
370
        }
Diego Gangl's avatar
Diego Gangl committed
371

Devin Lin's avatar
Devin Lin committed
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
        // ========== mobile delegate ==========
        Component {
            id: mobileDelegateComponent
            MobilePlayListDelegate {
                property var model
                width: parent ? parent.width : topItem.width

                index: model ? model.index : 0
                isAlternateColor: playListView.currentIndex % 2
                isSelected: playListView.currentIndex === index

                databaseId: model && model.databaseId ? model.databaseId : 0
                entryType: model && model.entryType ? model.entryType : ElisaUtils.Unknown
                title: model ? model.title || '' : ''
                artist: model ? model.artist || '' : ''
                album: model ? model.album || '' : ''
                albumArtist: model ? model.albumArtist || '' : ''
                duration: model ? model.duration || '' : ''
                fileName: model ? model.trackResource || '' : ''
                imageUrl: model ? model.imageUrl || '' : ''
                trackNumber: model ? model.trackNumber || -1 : -1
                discNumber: model ? model.discNumber || -1 : -1
                rating: model ? model.rating || 0 : 0
                isSingleDiscAlbum: model && model.isSingleDiscAlbum !== undefined ? model.isSingleDiscAlbum : true
                isValid: model && model.isValid
                isPlaying: model ? model.isPlaying : false
                metadataModifiableRole: model ? model.metadataModifiableRole : false
                hideDiscNumber: model && model.isSingleDiscAlbum

                listView: playListView

                onStartPlayback: topItem.startPlayback()
                onPausePlayback: topItem.pausePlayback()
                onRemoveFromPlaylist: ElisaApplication.mediaPlayListProxyModel.removeRow(trackIndex)
                onSwitchToTrack: ElisaApplication.mediaPlayListProxyModel.switchTo(trackIndex)

                onActiveFocusChanged: {
                    if (activeFocus && playListView.currentIndex !== index) {
                        playListView.currentIndex = index
                    }
                }
413

Devin Lin's avatar
Devin Lin committed
414
415
416
                onClicked: {
                    playListView.currentIndex = index
                    forceActiveFocus()
Diego Gangl's avatar
Diego Gangl committed
417

Devin Lin's avatar
Devin Lin committed
418
419
420
421
422
423
424
425
426
                    if (model.isValid) {
                        if (model.isPlaying === MediaPlayList.IsPlaying) {
                            topItem.pausePlayback()
                        } else {
                            ElisaApplication.mediaPlayListProxyModel.switchTo(model.index)
                            topItem.startPlayback()
                        }
                    }
                }
Diego Gangl's avatar
Diego Gangl committed
427

Devin Lin's avatar
Devin Lin committed
428
429
                onMoveItemRequested: {
                    ElisaApplication.mediaPlayListProxyModel.moveRow(from, to);
430
                }
Devin Lin's avatar
Devin Lin committed
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
            }
        }

        // ========== mobile listview ==========
        Component {
            id: mobileListView
            ScrollView {
                property alias list: playListView
                ListView {
                    id: playListView
                    anchors.fill: parent
                    reuseItems: true

                    model: ElisaApplication.mediaPlayListProxyModel

                    moveDisplaced: Transition {
                        YAnimator {
                            duration: Kirigami.Units.longDuration
                            easing.type: Easing.InOutQuad
                        }
                    }
452

Devin Lin's avatar
Devin Lin committed
453
454
455
456
457
458
459
                    Kirigami.PlaceholderMessage {
                        anchors.centerIn: parent
                        anchors.left: parent.left
                        anchors.right: parent.right
                        anchors.margins: Kirigami.Units.largeSpacing
                        width: parent.width - (Kirigami.Units.largeSpacing * 4)
                        visible: ElisaApplication.mediaPlayListProxyModel ? ElisaApplication.mediaPlayListProxyModel.tracksCount === 0 : true
460

Devin Lin's avatar
Devin Lin committed
461
462
                        icon.name: "view-media-playlist"
                        text: xi18nc("@info", "Your playlist is empty.")
463
                    }
464

Devin Lin's avatar
Devin Lin committed
465
466
467
468
469
470
471
472
473
                    delegate: Loader {
                        // apparently it's possible for parent to be null, set to undefined to ignore warning
                        anchors.left: parent ? parent.left : undefined
                        anchors.right: parent ? parent.right : undefined
                        sourceComponent: mobileDelegateComponent
                        onLoaded: {
                            item.model = model;
                        }
                    }
474
475
                }
            }
476
        }
Devin Lin's avatar
Devin Lin committed
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500

        Loader {
            id: playListLoader
            Layout.fillWidth: true
            Layout.fillHeight: true
            sourceComponent: Kirigami.Settings.isMobile ? mobileListView : desktopListView
            onLoaded: playListView = item.list
        }

        Kirigami.InlineMessage {
            id: mobileClearedMessage
            Layout.fillWidth: true
            visible: false
            showCloseButton: true
            text: i18nc("Playlist cleared", "Playlist cleared")

            actions: [
                Kirigami.Action {
                    text: i18n("Undo")
                    icon.name: "edit-undo-symbolic"
                    onTriggered: ElisaApplication.mediaPlayListProxyModel.undoClearPlayList()
                }
            ]
        }
501
    }
502

503
504
    footer: ToolBar {
        implicitHeight: Math.round(Kirigami.Units.gridUnit * 2)
505

506
507
        RowLayout {
            anchors.fill: parent
508

509
510
511
512
513
514
            LabelWithToolTip {
                text: i18np("%1 track", "%1 tracks", (ElisaApplication.mediaPlayListProxyModel ? ElisaApplication.mediaPlayListProxyModel.tracksCount : 0))
                elide: Text.ElideLeft
            }
            Item {
                Layout.fillWidth: true
515
            }
516
517
            LabelWithToolTip {
                visible: ElisaApplication.mediaPlayListProxyModel.remainingTracks != -1
518

519
520
                text: ElisaApplication.mediaPlayListProxyModel.remainingTracks == 0 ? i18n("Last track") : i18ncp("Number of remaining tracks in a playlist of songs", "%1 remaining", "%1 remaining", ElisaApplication.mediaPlayListProxyModel.remainingTracks)
                elide: Text.ElideRight
521
            }
522
        }
523
524
525
526
527
    }

    PlatformDialog.FileDialog {
        id: fileDialog

528
        defaultSuffix: 'm3u8'
529
        folder: PlatformDialog.StandardPaths.writableLocation(PlatformDialog.StandardPaths.MusicLocation)
530
        nameFilters: [i18nc("file type (mime type) for m3u and m3u8 playlist file formats", "Playlist (*.m3u*)")]
531

532
533
534
535
536
        onAccepted:
        {
            if (fileMode === PlatformDialog.FileDialog.SaveFile) {
                if (!ElisaApplication.mediaPlayListProxyModel.savePlayList(fileDialog.file)) {
                    showPlayListNotification(i18nc("Message when saving a playlist failed", "Saving failed"), Kirigami.MessageType.Error, retrySaveAction)
537
                }
538
539
540
            } else {
                ElisaApplication.mediaPlayListProxyModel.loadPlayList(fileDialog.file)
            }
541
        }
542
543
544
    }
}