diff --git a/src/MediaPlayListView.qml b/src/MediaPlayListView.qml index 57f07a09..c5d7eeef 100644 --- a/src/MediaPlayListView.qml +++ b/src/MediaPlayListView.qml @@ -1,357 +1,392 @@ /* * Copyright 2016-2017 Matthieu Gallien * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ import QtQuick 2.5 import QtQuick.Controls 1.3 import QtQuick.Controls.Styles 1.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.elisa 1.0 FocusScope { property StackView parentStackView property MediaPlayList playListModel property var playListControler property alias randomPlayChecked: shuffleOption.checked property alias repeatPlayChecked: repeatOption.checked property int placeholderHeight: elisaTheme.dragDropPlaceholderHeight signal startPlayback() signal pausePlayback() signal displayError(var errorText) id: topItem Action { id: clearPlayList text: i18nc("Remove all tracks from play list", "Clear Play List") iconName: "list-remove" enabled: playListModelDelegate.items.count > 0 onTriggered: { var selectedItems = [] var myGroup = playListModelDelegate.items for (var i = 0; i < myGroup.count; ++i) { var myItem = myGroup.get(i) selectedItems.push(myItem.itemsIndex) } playListModel.removeSelection(selectedItems) } } Action { id: showCurrentTrack text: i18nc("Show currently played track inside playlist", "Show Current Track") iconName: 'media-show-active-track-amarok' enabled: playListModelDelegate.items.count > 0 - onTriggered: playListView.positionViewAtIndex(playListControler.currentTrackRow, ListView.Contain) + onTriggered: { + playListView.positionViewAtIndex(playListControler.currentTrackRow, ListView.Contain) + playListView.currentIndex = playListControler.currentTrackRow + playListView.currentItem.forceActiveFocus() + } } Action { id: loadPlaylist text: i18nc("Load a playlist file", "Load a Playlist") iconName: 'document-open' onTriggered: { fileDialog.fileMode = PlatformDialog.FileDialog.OpenFile fileDialog.file = '' fileDialog.open() } } Action { id: savePlaylist text: i18nc("Save a playlist file", "Save a Playlist") iconName: 'document-save' enabled: playListModelDelegate.items.count > 0 onTriggered: { fileDialog.fileMode = PlatformDialog.FileDialog.SaveFile fileDialog.file = '' fileDialog.open() } } 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 (!playListModel.savePlaylist(fileDialog.file)) { displayError(i18nc("message of passive notification when playlist load failed", "Save of playlist failed")) } } else { playListModel.loadPlaylist(fileDialog.file) } } } ColumnLayout { anchors.fill: parent spacing: 0 ColumnLayout { height: elisaTheme.navigationBarHeight Layout.preferredHeight: elisaTheme.navigationBarHeight Layout.minimumHeight: elisaTheme.navigationBarHeight Layout.maximumHeight: elisaTheme.navigationBarHeight spacing: 0 TextMetrics { id: titleHeight text: viewTitleHeight.text font { pixelSize: viewTitleHeight.font.pixelSize bold: viewTitleHeight.font.bold } } LabelWithToolTip { id: viewTitleHeight text: i18nc("Title of the view of the playlist", "Now Playing") color: myPalette.text font.pixelSize: elisaTheme.defaultFontPixelSize * 2 Layout.topMargin: elisaTheme.layoutVerticalMargin Layout.leftMargin: elisaTheme.layoutHorizontalMargin Layout.rightMargin: elisaTheme.layoutHorizontalMargin } Item { Layout.fillHeight: true } RowLayout { Layout.fillWidth: true Layout.bottomMargin: elisaTheme.layoutVerticalMargin Layout.leftMargin: elisaTheme.layoutHorizontalMargin Layout.rightMargin: elisaTheme.layoutHorizontalMargin CheckBox { id: shuffleOption text: i18n("Shuffle") } CheckBox { id: repeatOption text: i18n("Repeat") Layout.leftMargin: !LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 Layout.rightMargin: LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 } Item { Layout.fillWidth: true } LabelWithToolTip { id: playListInfo text: i18np("1 track", "%1 tracks", playListModel.tracksCount) color: myPalette.text Layout.alignment: Qt.AlignRight } } } ScrollView { flickableItem.boundsBehavior: Flickable.StopAtBounds Layout.fillWidth: true Layout.fillHeight: true focus: true ListView { id: playListView focus: true TextEdit { readOnly: true visible: playListModelDelegate.count === 0 wrapMode: TextEdit.Wrap renderType: TextEdit.NativeRendering color: myPalette.text font.weight: Font.ExtraLight font.pointSize: 12 text: i18nc("Text shown when play list is empty", "Your play list is empty.\nIn order to start, you can explore your music library with the views on the left.\nUse the available buttons to add your selection.") anchors.fill: parent anchors.margins: elisaTheme.layoutHorizontalMargin } + add: Transition { + NumberAnimation { + property: "opacity"; + from: 0; + to: 1; + duration: 100 } + } + + populate: Transition { + NumberAnimation { + property: "opacity"; + from: 0; + to: 1; + duration: 100 } + } + + remove: Transition { + NumberAnimation { + property: "opacity"; + from: 1.0; + to: 0; + duration: 100 } + } + + displaced: Transition { + NumberAnimation { + properties: "x,y"; + duration: 100; + easing.type: Easing.InOutQuad} + } + model: DelegateModel { id: playListModelDelegate model: playListModel groups: [ DelegateModelGroup { name: "selected" } ] delegate: DraggableItem { id: item - placeholderHeight: topItem.placeholderHeight focus: true PlayListEntry { id: entry focus: true width: playListView.width index: model.index isAlternateColor: item.DelegateModel.itemsIndex % 2 hasAlbumHeader: if (model != undefined && model.hasAlbumHeader !== undefined) model.hasAlbumHeader else true title: if (model != undefined && model.title !== undefined) model.title else '' artist: if (model != undefined && model.artist !== undefined) model.artist else '' albumArtist: if (model != undefined && model.albumArtist !== undefined) - model.albumArtist - else - '' + model.albumArtist + else + '' itemDecoration: if (model != undefined && model.image !== undefined) model.image else '' duration: if (model != undefined && model.duration !== undefined) model.duration else '' trackNumber: if (model != undefined && model.trackNumber !== undefined) model.trackNumber else '' discNumber: if (model != undefined && model.discNumber !== undefined) model.discNumber else '' isSingleDiscAlbum: if (model != undefined && model.isSingleDiscAlbum !== undefined) model.isSingleDiscAlbum else false album: if (model != undefined && model.album !== undefined) model.album else '' rating: if (model != undefined && model.rating !== undefined) model.rating else 0 isValid: model.isValid isPlaying: model.isPlaying isSelected: playListView.currentIndex === index containsMouse: item.containsMouse - playListModel: topItem.playListModel - playListControler: topItem.playListControler - onStartPlayback: topItem.startPlayback() onPausePlayback: topItem.pausePlayback() + + onRemoveFromPlaylist: topItem.playListModel.removeRows(trackIndex, 1) + + onSwitchToTrack: topItem.playListControler.switchTo(trackIndex) } draggedItemParent: topItem onClicked: { playListView.currentIndex = index entry.forceActiveFocus() } onDoubleClicked: { if (model.isValid) { topItem.playListControler.switchTo(model.index) topItem.startPlayback() } } onMoveItemRequested: { playListModel.move(from, to, 1); } } } } } Rectangle { id: actionBar Layout.fillWidth: true Layout.topMargin: elisaTheme.layoutVerticalMargin Layout.preferredHeight: elisaTheme.delegateToolButtonSize color: myPalette.mid RowLayout { id: actionBarLayout anchors.fill: parent ToolButton { action: clearPlayList Layout.bottomMargin: elisaTheme.layoutVerticalMargin } ToolButton { action: showCurrentTrack Layout.bottomMargin: elisaTheme.layoutVerticalMargin } ToolButton { action: loadPlaylist Layout.bottomMargin: elisaTheme.layoutVerticalMargin } ToolButton { action: savePlaylist Layout.bottomMargin: elisaTheme.layoutVerticalMargin } Item { Layout.fillWidth: true } } } } } diff --git a/src/PlayListEntry.qml b/src/PlayListEntry.qml index 37964dbc..b019e4e0 100644 --- a/src/PlayListEntry.qml +++ b/src/PlayListEntry.qml @@ -1,417 +1,409 @@ /* * Copyright 2016-2017 Matthieu Gallien * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ import QtQuick 2.5 import QtQuick.Layouts 1.2 import QtQuick.Controls 1.2 import QtQuick.Window 2.2 import QtGraphicalEffects 1.0 import org.kde.elisa 1.0 FocusScope { id: playListEntry property string title property string artist property string album property string albumArtist property var index property var itemDecoration property alias duration : durationLabel.text property int trackNumber property int discNumber property bool isSingleDiscAlbum property alias rating: ratingWidget.starRating property int isPlaying property bool isSelected property bool isValid property bool isAlternateColor property bool containsMouse property bool hasAlbumHeader - property var playListModel - property var playListControler signal startPlayback() signal pausePlayback() + signal removeFromPlaylist(var trackIndex) + signal switchToTrack(var trackIndex) + + height: (hasAlbumHeader ? elisaTheme.delegateWithHeaderHeight : elisaTheme.delegateHeight) Action { id: removeFromPlayList text: i18nc("Remove current track from play list", "Remove") iconName: "list-remove" onTriggered: { - playListModel.removeRows(playListEntry.index, 1) + playListEntry.removeFromPlaylist(playListEntry.index) } } Action { id: playNow text: i18nc("Play now current track from play list", "Play Now") iconName: "media-playback-start" enabled: !(isPlaying == MediaPlayList.IsPlaying) && isValid onTriggered: { - playListControler.switchTo(playListEntry.index) + playListEntry.switchToTrack(playListEntry.index) playListEntry.startPlayback() } } Action { id: pauseNow text: i18nc("Pause current track from play list", "Pause") iconName: "media-playback-pause" enabled: isPlaying == MediaPlayList.IsPlaying && isValid onTriggered: playListEntry.pausePlayback() } Rectangle { id: entryBackground anchors.fill: parent color: (isAlternateColor ? myPalette.alternateBase : myPalette.base) height: (hasAlbumHeader ? elisaTheme.delegateWithHeaderHeight : elisaTheme.delegateHeight) focus: true ColumnLayout { spacing: 0 anchors.fill: parent anchors.leftMargin: elisaTheme.layoutHorizontalMargin anchors.rightMargin: elisaTheme.layoutHorizontalMargin - anchors.topMargin: 0 - anchors.bottomMargin: 1 Item { Layout.fillWidth: true Layout.preferredHeight: elisaTheme.delegateWithHeaderHeight - elisaTheme.delegateHeight Layout.minimumHeight: elisaTheme.delegateWithHeaderHeight - elisaTheme.delegateHeight Layout.maximumHeight: elisaTheme.delegateWithHeaderHeight - elisaTheme.delegateHeight visible: hasAlbumHeader RowLayout { id: headerRow spacing: elisaTheme.layoutHorizontalMargin anchors.fill: parent Image { id: mainIcon source: (isValid ? (playListEntry.itemDecoration ? playListEntry.itemDecoration : Qt.resolvedUrl(elisaTheme.albumCover)) : Qt.resolvedUrl(elisaTheme.errorIcon)) Layout.minimumWidth: headerRow.height - 4 Layout.maximumWidth: headerRow.height - 4 Layout.preferredWidth: headerRow.height - 4 Layout.minimumHeight: headerRow.height - 4 Layout.maximumHeight: headerRow.height - 4 Layout.preferredHeight: headerRow.height - 4 Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter sourceSize.width: headerRow.height - 4 sourceSize.height: parent.height - 4 fillMode: Image.PreserveAspectFit asynchronous: true visible: isValid } BrightnessContrast { source: mainIcon cached: true visible: !isValid contrast: -0.9 Layout.minimumWidth: headerRow.height - 4 Layout.maximumWidth: headerRow.height - 4 Layout.preferredWidth: headerRow.height - 4 Layout.minimumHeight: headerRow.height - 4 Layout.maximumHeight: headerRow.height - 4 Layout.preferredHeight: headerRow.height - 4 Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter } ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true spacing: 0 LabelWithToolTip { id: mainLabel text: album font.weight: Font.Bold color: myPalette.text - horizontalAlignment: "AlignHCenter" + horizontalAlignment: Text.AlignHCenter Layout.fillWidth: true Layout.alignment: Qt.AlignCenter Layout.topMargin: elisaTheme.layoutVerticalMargin elide: Text.ElideRight } Item { Layout.fillHeight: true } LabelWithToolTip { id: authorLabel text: albumArtist font.weight: Font.Light color: myPalette.text - horizontalAlignment: "AlignHCenter" + horizontalAlignment: Text.AlignHCenter Layout.fillWidth: true Layout.alignment: Qt.AlignCenter Layout.bottomMargin: elisaTheme.layoutVerticalMargin elide: Text.ElideRight } } } } Item { Layout.fillWidth: true Layout.fillHeight: true RowLayout { id: trackRow anchors.fill: parent spacing: elisaTheme.layoutHorizontalMargin LabelWithToolTip { id: mainCompactLabel text: ((discNumber && !isSingleDiscAlbum) ? discNumber + ' - ' + trackNumber : trackNumber) + ' - ' + title font.weight: (isPlaying ? Font.Bold : Font.Normal) color: myPalette.text Layout.maximumWidth: mainCompactLabel.implicitWidth + 1 Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft visible: isValid elide: Text.ElideRight } LabelWithToolTip { id: mainInvalidCompactLabel text: title font.weight: Font.Normal color: myPalette.text Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft visible: !isValid elide: Text.ElideRight } Item { Layout.fillWidth: true Layout.preferredWidth: 0 } ToolButton { id: playPauseButton implicitHeight: elisaTheme.smallDelegateToolButtonSize implicitWidth: elisaTheme.smallDelegateToolButtonSize opacity: 0 visible: opacity > 0.1 action: !(isPlaying == MediaPlayList.IsPlaying) ? playNow : pauseNow Layout.alignment: Qt.AlignVCenter | Qt.AlignRight } Item { implicitHeight: elisaTheme.smallDelegateToolButtonSize implicitWidth: elisaTheme.smallDelegateToolButtonSize Layout.maximumWidth: elisaTheme.smallDelegateToolButtonSize Layout.maximumHeight: elisaTheme.smallDelegateToolButtonSize Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter ToolButton { id: removeButton anchors.fill: parent opacity: 0 visible: opacity > 0.1 action: removeFromPlayList } Image { id: playIcon anchors.fill: parent opacity: 0 source: (isPlaying == MediaPlayList.IsPlaying ? Qt.resolvedUrl(elisaTheme.playIcon) : Qt.resolvedUrl(elisaTheme.pauseIcon)) width: parent.height * 1. height: parent.height * 1. sourceSize.width: parent.height * 1. sourceSize.height: parent.height * 1. fillMode: Image.PreserveAspectFit mirror: LayoutMirroring.enabled visible: isPlaying == MediaPlayList.IsPlaying || isPlaying == MediaPlayList.IsPaused SequentialAnimation on opacity { running: isPlaying == MediaPlayList.IsPlaying && playListEntry.state != 'hoveredOrSelected' loops: Animation.Infinite NumberAnimation { from: 0 to: 1. duration: 1000 easing.type: Easing.InOutCubic } NumberAnimation { from: 1 to: 0 duration: 1000 easing.type: Easing.InOutCubic } } SequentialAnimation on opacity { running: isPlaying == MediaPlayList.IsPaused && playListEntry.state != 'hoveredOrSelected' NumberAnimation { from: playIcon.opacity to: 1. duration: 1000 easing.type: Easing.InOutCubic } } SequentialAnimation on opacity { running: playListEntry.state == 'hoveredOrSelected' NumberAnimation { from: playIcon.opacity to: 0 - duration: 50 + duration: 250 easing.type: Easing.InOutCubic } } } } RatingStar { id: ratingWidget starSize: elisaTheme.ratingStarSize } LabelWithToolTip { id: durationLabel text: duration color: myPalette.text elide: Text.ElideRight Layout.alignment: Qt.AlignVCenter | Qt.AlignRight } } } } } states: [ State { name: 'notSelected' when: !containsMouse && (!playListEntry.activeFocus || !isSelected) - PropertyChanges { - target: playListEntry - height: (hasAlbumHeader ? elisaTheme.delegateWithHeaderHeight : elisaTheme.delegateHeight) - } PropertyChanges { target: removeButton opacity: 0 } PropertyChanges { target: playPauseButton opacity: 0 } PropertyChanges { target: entryBackground color: (isAlternateColor ? myPalette.alternateBase : myPalette.base) } }, State { name: 'hoveredOrSelected' when: containsMouse || (playListEntry.activeFocus && isSelected) - PropertyChanges { - target: playListEntry - height: (hasAlbumHeader ? elisaTheme.delegateWithHeaderHeight : elisaTheme.delegateHeight) - } PropertyChanges { target: removeButton opacity: 1 } PropertyChanges { target: playPauseButton opacity: 1 } PropertyChanges { target: entryBackground color: myPalette.mid } } ] transitions: Transition { ParallelAnimation { NumberAnimation { - properties: "height, opacity" + properties: "opacity" easing.type: Easing.InOutQuad - duration: 50 + duration: 250 } ColorAnimation { properties: "color" - duration: 200 + duration: 250 } } } }