Commit 41793eda authored by Nate Graham's avatar Nate Graham 💤
Browse files

Port playlist panel to Kirigami.Page

This commit ports the PlayList to use a Kirgami.Page with proper
headers and footers, and ports the HeaderFooterToolbar components to use
QQC2.ToolBar with Kirigami.ActionToolbars inside them.

There are no UI changes.
parent bedc50ef
Pipeline #51133 canceled with stage
......@@ -416,7 +416,6 @@ if (Qt5Quick_FOUND AND Qt5Widgets_FOUND)
qml/DataListView.qml
qml/MediaPlayListView.qml
qml/PlayListBasicView.qml
qml/PlayListEntry.qml
qml/SimplePlayListView.qml
qml/BasicPlayListAlbumHeader.qml
......
......@@ -5,22 +5,27 @@
SPDX-License-Identifier: LGPL-3.0-or-later
*/
import QtQuick 2.5
import QtQuick 2.10
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.1
import QtQuick.Window 2.2
import QtQml.Models 2.1
import Qt.labs.platform 1.0 as PlatformDialog
import org.kde.kirigami 2.12 as Kirigami
import org.kde.kirigami 2.15 as Kirigami
import org.kde.elisa 1.0
FocusScope {
property StackView parentStackView
property int placeholderHeight: elisaTheme.dragDropPlaceholderHeight
// Not using ScrollablePage because we don't need any of the refresh features
// that it provides
Kirigami.Page {
id: topItem
signal startPlayback()
signal pausePlayback()
function hideNotification() {
playListNotification.visible = false;
}
function showPlayListNotification(message, type, action) {
if (!message) {
return;
......@@ -42,9 +47,17 @@ FocusScope {
playListNotification.visible = true;
}
function hideNotification() {
playListNotification.visible = false;
}
title: i18nc("@info Title of the view of the playlist; keep this string as short as possible because horizontal space is quite scarce", "Playlist")
globalToolBarStyle: Kirigami.ApplicationHeaderStyle.ToolBar
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
Kirigami.Action {
id: undoAction
......@@ -88,177 +101,287 @@ FocusScope {
}
}
id: topItem
Accessible.role: Accessible.Pane
Accessible.name: viewTitle.text
PlatformDialog.FileDialog {
id: fileDialog
defaultSuffix: 'm3u'
folder: PlatformDialog.StandardPaths.writableLocation(PlatformDialog.StandardPaths.MusicLocation)
nameFilters: [i18nc("file type (mime type) for m3u playlist", "Playlist (*.m3u)")]
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)
}
} else {
ElisaApplication.mediaPlayListProxyModel.loadPlayList(fileDialog.file)
// FIXME: Apparently the page needs to have a row or a stack to show the
// header toolbar automatically, so we have to make our own See
// https://bugs.kde.org/show_bug.cgi?id=432541
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
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()
}
]
}
}
}
ColumnLayout {
ScrollView {
anchors.fill: parent
spacing: 0
// Header with title and toolbar buttons
HeaderFooterToolbar {
Layout.fillWidth: true
toolbarType: HeaderFooterToolbar.ToolbarType.Header
contentItems: [
ListView {
id: playListView
// Header title
LabelWithToolTip {
id: viewTitle
focus: true
clip: true
keyNavigationEnabled: true
activeFocusOnTab: true
Layout.fillWidth: true
currentIndex: -1
text: i18nc("@info Title of the view of the playlist; keep this string as short as possible because horizontal space is quite scarce", "Playlist")
Accessible.role: Accessible.List
Accessible.name: topItem.title
level: 1
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
},
section.property: 'albumSection'
section.criteria: ViewSection.FullString
section.labelPositioning: ViewSection.InlineLabels
section.delegate: BasicPlayListAlbumHeader {
headerData: JSON.parse(section)
width: playListView.width
}
// Toolbar buttons
FlatButtonWithToolTip {
text: i18nc("Show currently played track inside playlist", "Show Current Track")
icon.name: 'media-track-show-active'
enabled: ElisaApplication.mediaPlayListProxyModel ? ElisaApplication.mediaPlayListProxyModel.tracksCount > 0 : false
onClicked: {
playListView.listView.positionViewAtIndex(ElisaApplication.mediaPlayListProxyModel.currentTrackRow, ListView.Contain)
playListView.listView.currentIndex = ElisaApplication.mediaPlayListProxyModel.currentTrackRow
playListView.listView.currentItem.forceActiveFocus()
}
},
FlatButtonWithToolTip {
id: savePlaylistButton
text: i18nc("Save a playlist file", "Save Playlist...")
icon.name: 'document-save'
enabled: ElisaApplication.mediaPlayListProxyModel ? ElisaApplication.mediaPlayListProxyModel.tracksCount > 0 : false
onClicked: {
fileDialog.fileMode = PlatformDialog.FileDialog.SaveFile
fileDialog.file = ''
fileDialog.open()
/* 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 }
}
populate: Transition {
NumberAnimation {
property: "opacity";
from: 0;
to: 1;
duration: Kirigami.Units.shortDuration }
}
remove: Transition {
NumberAnimation {
property: "opacity";
from: 1.0;
to: 0;
duration: Kirigami.Units.shortDuration }
}
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.metadataModifiableRole
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
}
}
}
},
FlatButtonWithToolTip {
id: loadPlaylistButton
text: i18nc("Load a playlist file", "Load Playlist...")
icon.name: 'document-open'
draggedItemParent: playListView
onClicked: {
fileDialog.fileMode = PlatformDialog.FileDialog.OpenFile
fileDialog.file = ''
fileDialog.open()
playListView.currentIndex = index
entry.forceActiveFocus()
}
},
FlatButtonWithToolTip {
text: i18nc("Remove all tracks from play list", "Clear Playlist")
icon.name: 'edit-clear-all'
enabled: ElisaApplication.mediaPlayListProxyModel ? ElisaApplication.mediaPlayListProxyModel.tracksCount > 0 : false
onClicked: ElisaApplication.mediaPlayListProxyModel.clearPlayList()
}
]
}
Item {
id: emptyPlaylistMessage
onDoubleClicked: {
if (model.isValid) {
ElisaApplication.mediaPlayListProxyModel.switchTo(model.index)
topItem.startPlayback()
}
}
visible: ElisaApplication.mediaPlayListProxyModel ? ElisaApplication.mediaPlayListProxyModel.tracksCount === 0 : true
clip: true
onMoveItemRequested: {
ElisaApplication.mediaPlayListProxyModel.moveRow(from, to);
}
}
}
Layout.fillHeight: true
Layout.fillWidth: true
onCountChanged: if (count === 0) {
currentIndex = -1;
}
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
}
}
PlayListBasicView {
id: playListView
visible: !emptyPlaylistMessage.visible
Layout.fillWidth: true
Layout.fillHeight: true
title: viewTitle.text
playListModel: ElisaApplication.mediaPlayListProxyModel
Kirigami.InlineMessage {
id: playListNotification
focus: true
onStartPlayback: topItem.startPlayback()
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
margins: Kirigami.Units.largeSpacing
}
onPausePlayback: topItem.pausePlayback()
type: Kirigami.MessageType.Information
showCloseButton: true
onDisplayError: showPlayListNotification(errorText, Kirigami.MessageType.Error)
onVisibleChanged: {
if (visible) {
autoHideNotificationTimer.start()
} else {
autoHideNotificationTimer.stop()
}
}
Timer {
id: autoHideNotificationTimer
interval: 7000
onTriggered: playListNotification.visible = false
}
}
}
}
Kirigami.InlineMessage {
id: playListNotification
Timer {
id: autoHideNotificationTimer
footer: ToolBar {
implicitHeight: Math.round(Kirigami.Units.gridUnit * 2)
interval: 7000
RowLayout {
anchors.fill: parent
onTriggered: playListNotification.visible = false
LabelWithToolTip {
text: i18np("%1 track", "%1 tracks", (ElisaApplication.mediaPlayListProxyModel ? ElisaApplication.mediaPlayListProxyModel.tracksCount : 0))
elide: Text.ElideLeft
}
Item {
Layout.fillWidth: true
}
LabelWithToolTip {
visible: ElisaApplication.mediaPlayListProxyModel.remainingTracks != -1
type: Kirigami.MessageType.Information
showCloseButton: true
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing
onVisibleChanged:
{
if (visible) {
autoHideNotificationTimer.start()
} else {
autoHideNotificationTimer.stop()
}
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
}
}
}
PlatformDialog.FileDialog {
id: fileDialog
defaultSuffix: 'm3u'
folder: PlatformDialog.StandardPaths.writableLocation(PlatformDialog.StandardPaths.MusicLocation)
nameFilters: [i18nc("file type (mime type) for m3u playlist", "Playlist (*.m3u)")]
// Footer with number of tracks label
HeaderFooterToolbar {
toolbarType: HeaderFooterToolbar.ToolbarType.Footer
Layout.fillWidth: true
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
contentItems: [
LabelWithToolTip {
text: i18np("%1 track", "%1 tracks", (ElisaApplication.mediaPlayListProxyModel ? ElisaApplication.mediaPlayListProxyModel.tracksCount : 0))
elide: Text.ElideLeft
},
Item {
Layout.fillWidth: true
},
LabelWithToolTip {
visible: ElisaApplication.mediaPlayListProxyModel.remainingTracks != -1
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
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)
}
]
} else {
ElisaApplication.mediaPlayListProxyModel.loadPlayList(fileDialog.file)
}
}
}
}
......
/*
SPDX-FileCopyrightText: 2016 (c) Matthieu Gallien <matthieu_gallien@yahoo.fr>
SPDX-License-Identifier: LGPL-3.0-or-later
*/
import QtQuick 2.10
import QtQuick.Controls 2.2
import QtQml.Models 2.1
import org.kde.elisa 1.0
ScrollView {
id: scrollView
property alias playListModel: playListModelDelegate.model
property alias listView: playListView
property string title
signal startPlayback()
signal pausePlayback()
signal displayError(var errorText)
ListView {
id: playListView
focus: true
clip: true
keyNavigationEnabled: true
activeFocusOnTab: true
currentIndex: -1
Accessible.role: Accessible.List
Accessible.name: scrollView.title
section.property: 'albumSection'
section.criteria: ViewSection.FullString
section.labelPositioning: ViewSection.InlineLabels
section.delegate: BasicPlayListAlbumHeader {
headerData: JSON.parse(section)
width: playListView.width
}
/* 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 }
}
populate: Transition {
NumberAnimation {
property: "opacity";
from: 0;
to: 1;
duration: Kirigami.Units.shortDuration }
}
remove: Transition {
NumberAnimation {
property: "opacity";
from: 1.0;
to: 0;
duration: Kirigami.Units.shortDuration }
}
displaced: Transition {
NumberAnimation {
properties: "x,y";
duration: Kirigami.Units.shortDuration
easing.type: Easing.InOutQuad }
}
*/
model: DelegateModel {
id: playListModelDelegate
groups: [
DelegateModelGroup { name: "selected" }
]
delegate: DraggableItem {
id: item
width: playListView.width
placeholderHeight: topItem.placeholderHeight
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.metadataModifiableRole
onStartPlayback: scrollView.startPlayback()
onPausePlayback: scrollView.pausePlayback()
onRemoveFromPlaylist: scrollView.playListModel.removeRow(trackIndex)