diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -345,6 +345,7 @@ ../src/musicalbum.cpp ../src/musicaudiotrack.cpp ../src/allalbumsmodel.cpp + ../src/albummodel.cpp allalbumsmodeltest.cpp ) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -44,8 +44,6 @@ RatingStar.qml PlayListEntry.qml - MediaAlbumDelegate.qml - MediaArtistDelegate.qml MediaBrowser.qml DraggableItem.qml PassiveNotification.qml @@ -59,14 +57,13 @@ MediaPlayerControl.qml ContextView.qml - MediaArtistAlbumView.qml MediaPlayListView.qml MediaAlbumView.qml - MediaAllAlbumView.qml - MediaAllArtistView.qml MediaAllTracksView.qml MediaTrackDelegate.qml MediaAlbumTrackDelegate.qml + GridBrowserView.qml + GridBrowserDelegate.qml ViewSelector.qml ) diff --git a/src/MediaArtistDelegate.qml b/src/GridBrowserDelegate.qml rename from src/MediaArtistDelegate.qml rename to src/GridBrowserDelegate.qml --- a/src/MediaArtistDelegate.qml +++ b/src/GridBrowserDelegate.qml @@ -17,68 +17,57 @@ * Boston, MA 02110-1301, USA. */ -import QtQuick 2.4 -import QtQuick.Controls 1.2 -import QtQuick.Controls.Styles 1.2 +import QtQuick 2.7 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 import QtQuick.Window 2.2 import QtQml.Models 2.1 import QtQuick.Layouts 1.2 import QtGraphicalEffects 1.0 import org.kde.elisa 1.0 -FocusScope { - id: mediaServerEntry +Item { + id: gridEntry - property StackView stackView - property MediaPlayList playListModel - property var musicListener - property var playerControl - property var contentDirectoryModel - property var image - property alias name: nameLabel.text + property var imageUrl + property bool shadowForImage + property alias mainText: mainLabel.text + property alias secondaryText: secondaryLabel.text + property var containerData + property bool delegateDisplaySecondaryText: true - signal artistClicked() - signal openArtist(var name) - - SystemPalette { - id: myPalette - colorGroup: SystemPalette.Active - } - - Theme { - id: elisaTheme - } + signal enqueue(var data); + signal enqueueAndPlay(var data); + signal open(); + signal selected(); Action { id: enqueueAction - text: i18nc("Add all tracks from artist to play list", "Enqueue") - iconName: "media-track-add-amarok" - onTriggered: mediaServerEntry.playListModel.enqueue(mediaServerEntry.name) + text: i18nc("Add whole container to play list", "Enqueue") + iconName: 'media-track-add-amarok' + onTriggered: enqueue(containerData) } Action { id: openAction - text: i18nc("Open artist view", "Open Artist") + text: i18nc("Open view of the container", "Open") iconName: 'document-open-folder' - onTriggered: openArtist(name) + onTriggered: open() } Action { id: enqueueAndPlayAction - text: i18nc("Clear play list and add all tracks from artist to play list", "Play Now and Replace Play List") - iconName: "media-playback-start" - onTriggered: { - mediaServerEntry.playListModel.clearAndEnqueue(mediaServerEntry.name) - mediaServerEntry.playerControl.ensurePlay() - } + text: i18nc("Clear play list and add whole container to play list", "Play Now and Replace Play List") + iconName: 'media-playback-start' + onTriggered: enqueueAndPlay(containerData) } - Keys.onReturnPressed: openArtist(name) - Keys.onEnterPressed: openArtist(name) + Keys.onReturnPressed: open() + Keys.onEnterPressed: open() ColumnLayout { anchors.fill: parent @@ -92,20 +81,26 @@ acceptedButtons: Qt.LeftButton focus: true - Layout.preferredHeight: mediaServerEntry.width * 0.85 + elisaTheme.layoutVerticalMargin * 0.5 + nameSize.height + Layout.preferredHeight: gridEntry.width * 0.85 + elisaTheme.layoutVerticalMargin * 0.5 + mainLabelSize.height + secondaryLabelSize.height Layout.fillWidth: true onClicked: { hoverHandle.forceActiveFocus() - artistClicked() + gridEntry.selected() } - onDoubleClicked: openArtist(name) + onDoubleClicked: open() TextMetrics { - id: nameSize - font: nameLabel.font - text: nameLabel.text + id: mainLabelSize + font: mainLabel.font + text: mainLabel.text + } + + TextMetrics { + id: secondaryLabelSize + font: secondaryLabel.font + text: secondaryLabel.text } ColumnLayout { @@ -115,11 +110,13 @@ anchors.fill: parent Item { - Layout.preferredWidth: mediaServerEntry.width * 0.85 - Layout.preferredHeight: mediaServerEntry.width * 0.85 + Layout.preferredHeight: gridEntry.width * 0.85 + Layout.preferredWidth: gridEntry.width * 0.85 Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + focus: true + Loader { id: hoverLoader active: false @@ -161,23 +158,23 @@ } Image { - id: artistDecoration - - source: Qt.resolvedUrl(elisaTheme.defaultArtistImage) + id: coverImage anchors.fill: parent sourceSize.width: parent.width sourceSize.height: parent.height - fillMode: Image.PreserveAspectFit - smooth: true + source: gridEntry.imageUrl + asynchronous: true - layer.enabled: image === '' ? false : true + layer.enabled: shadowForImage layer.effect: DropShadow { + source: coverImage + radius: 10 spread: 0.1 samples: 21 @@ -188,19 +185,35 @@ } LabelWithToolTip { - id: nameLabel + id: mainLabel font.weight: Font.Bold color: myPalette.text horizontalAlignment: Text.AlignLeft - Layout.preferredWidth: mediaServerEntry.width * 0.85 Layout.topMargin: elisaTheme.layoutVerticalMargin * 0.5 + Layout.preferredWidth: gridEntry.width * 0.85 Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom elide: Text.ElideRight } + + LabelWithToolTip { + id: secondaryLabel + + font.weight: Font.Light + color: myPalette.text + + horizontalAlignment: Text.AlignLeft + + Layout.preferredWidth: gridEntry.width * 0.85 + Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom + + visible: delegateDisplaySecondaryText + + elide: Text.ElideRight + } } } @@ -212,7 +225,7 @@ states: [ State { name: 'notSelected' - when: !mediaServerEntry.activeFocus && !hoverHandle.containsMouse + when: !gridEntry.activeFocus && !hoverHandle.containsMouse PropertyChanges { target: hoverLoader active: false @@ -222,13 +235,13 @@ opacity: 0.0 } PropertyChanges { - target: artistDecoration + target: coverImage opacity: 1 } }, State { name: 'hoveredOrSelected' - when: mediaServerEntry.activeFocus || hoverHandle.containsMouse + when: gridEntry.activeFocus || hoverHandle.containsMouse PropertyChanges { target: hoverLoader active: true @@ -238,7 +251,7 @@ opacity: 1.0 } PropertyChanges { - target: artistDecoration + target: coverImage opacity: 0.2 } } diff --git a/src/GridBrowserView.qml b/src/GridBrowserView.qml new file mode 100644 --- /dev/null +++ b/src/GridBrowserView.qml @@ -0,0 +1,171 @@ +/* + * 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.7 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import QtQuick.Window 2.2 +import QtQml.Models 2.1 +import QtQuick.Layouts 1.2 +import QtGraphicalEffects 1.0 + +import org.kde.elisa 1.0 + +FocusScope { + id: gridView + + property bool isFirstPage: false + property string mainTitle + property string secondaryTitle + property url image + property alias model: proxyModel.model + property bool showRating: true + property bool delegateDisplaySecondaryText: true + + signal enqueue(var data); + signal enqueueAndPlay(var data); + signal open(var innerModel, var innerMainTitle, var innerSecondaryTitle, var innerImage); + signal goBack() + + SystemPalette { + id: myPalette + colorGroup: SystemPalette.Active + } + + Theme { + id: elisaTheme + } + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + Loader { + sourceComponent: FilterBar { + id: filterBar + + labelText: mainTitle + showRating: gridView.showRating + + anchors.fill: parent + + Binding { + target: model + property: 'filterText' + value: filterBar.filterText + when: isFirstPage + } + + Binding { + target: model + property: 'filterRating' + value: filterBar.filterRating + when: isFirstPage + } + } + + height: elisaTheme.navigationBarHeight + Layout.preferredHeight: height + Layout.minimumHeight: height + Layout.maximumHeight: height + Layout.fillWidth: true + + active: isFirstPage + visible: isFirstPage + } + + Loader { + sourceComponent: NavigationActionBar { + id: navigationBar + + mainTitle: gridView.mainTitle + secondaryTitle: gridView.secondaryTitle + image: gridView.image + + anchors.fill: parent + + onEnqueue: gridView.enqueue(mainTitle) + onEnqueueAndPlay: gridView.enqueueAndPlay(mainTitle) + onGoBack: gridView.goBack() + } + + height: elisaTheme.navigationBarHeight + Layout.preferredHeight: height + Layout.minimumHeight: height + Layout.maximumHeight: height + Layout.fillWidth: true + + active: !isFirstPage + visible: !isFirstPage + } + + Rectangle { + color: myPalette.base + + Layout.fillHeight: true + Layout.fillWidth: true + + ScrollView { + anchors.fill: parent + flickableItem.boundsBehavior: Flickable.StopAtBounds + flickableItem.interactive: true + + GridView { + id: contentDirectoryView + + focus: true + + TextMetrics { + id: secondaryLabelSize + text: 'example' + } + + cellWidth: elisaTheme.gridDelegateWidth + cellHeight: (delegateDisplaySecondaryText ? elisaTheme.gridDelegateHeight : elisaTheme.gridDelegateHeight - secondaryLabelSize.height) + + model: DelegateModel { + id: proxyModel + + delegate: GridBrowserDelegate { + width: contentDirectoryView.cellWidth + height: contentDirectoryView.cellHeight + + focus: true + + mainText: model.display + secondaryText: model.secondaryText + imageUrl: model.imageUrl + shadowForImage: model.shadowForImage + containerData: model.containerData + delegateDisplaySecondaryText: gridView.delegateDisplaySecondaryText + + onEnqueue: gridView.enqueue(data) + onEnqueueAndPlay: gridView.enqueueAndPlay(data) + onOpen: gridView.open(model.childModel, model.display, model.secondaryText, model.imageUrl) + onSelected: { + forceActiveFocus() + contentDirectoryView.currentIndex = model.index + } + } + } + } + } + } + } +} diff --git a/src/MediaAlbumDelegate.qml b/src/MediaAlbumDelegate.qml deleted file mode 100644 --- a/src/MediaAlbumDelegate.qml +++ /dev/null @@ -1,318 +0,0 @@ -/* - * 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.4 -import QtQuick.Controls 1.2 -import QtQuick.Controls.Styles 1.2 -import QtQuick.Window 2.2 -import QtQml.Models 2.1 -import QtQuick.Layouts 1.2 -import QtGraphicalEffects 1.0 - -import org.kde.elisa 1.0 - -FocusScope { - id: mediaServerEntry - - property StackView stackView - property MediaPlayList playListModel - property var musicListener - property var playerControl - property var image - property alias title: titleLabel.text - property alias artist: artistLabel.text - property string trackNumber - property bool isSingleDiscAlbum - property var albumData - property var albumId - - signal showArtist(var name) - signal albumClicked() - - Action { - id: enqueueAction - - text: i18nc("Add whole album to play list", "Enqueue") - iconName: 'media-track-add-amarok' - onTriggered: mediaServerEntry.playListModel.enqueue(mediaServerEntry.albumData) - } - - Action { - id: openAction - - text: i18nc("Open album view", "Open Album") - iconName: 'document-open-folder' - onTriggered: showAlbumTracks() - } - - Action { - id: enqueueAndPlayAction - - text: i18nc("Clear play list and add whole album to play list", "Play Now and Replace Play List") - iconName: 'media-playback-start' - onTriggered: { - mediaServerEntry.playListModel.clearAndEnqueue(mediaServerEntry.albumData) - mediaServerEntry.playerControl.ensurePlay() - } - } - - Keys.onReturnPressed: showAlbumTracks() - Keys.onEnterPressed: showAlbumTracks() - - Component { - id: albumViewComponent - - MediaAlbumView { - stackView: mediaServerEntry.stackView - playListModel: mediaServerEntry.playListModel - musicListener: mediaServerEntry.musicListener - playerControl: mediaServerEntry.playerControl - albumArtUrl: image - albumName: title - artistName: artist - tracksCount: count - isSingleDiscAlbum: mediaServerEntry.isSingleDiscAlbum - albumData: mediaServerEntry.albumData - albumId: mediaServerEntry.albumId - - onShowArtist: mediaServerEntry.showArtist(name) - } - } - - function showAlbumTracks() { - stackView.push(albumViewComponent) - } - - ColumnLayout { - anchors.fill: parent - - spacing: 0 - - MouseArea { - id: hoverHandle - - hoverEnabled: true - acceptedButtons: Qt.LeftButton - focus: true - - Layout.preferredHeight: mediaServerEntry.width * 0.85 + elisaTheme.layoutVerticalMargin * 0.5 + titleSize.height + artistSize.height - Layout.fillWidth: true - - onClicked: { - hoverHandle.forceActiveFocus() - albumClicked() - } - - onDoubleClicked: showAlbumTracks() - - TextMetrics { - id: titleSize - font: titleLabel.font - text: titleLabel.text - } - - TextMetrics { - id: artistSize - font: artistLabel.font - text: artistLabel.text - } - - ColumnLayout { - id: mainData - - spacing: 0 - anchors.fill: parent - - Item { - Layout.preferredHeight: mediaServerEntry.width * 0.85 - Layout.preferredWidth: mediaServerEntry.width * 0.85 - - Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter - - focus: true - - Loader { - id: hoverLoader - active: false - - anchors.centerIn: parent - z: 1 - - opacity: 0 - - sourceComponent: Row { - - ToolButton { - id: enqueueButton - - action: enqueueAction - - width: elisaTheme.delegateToolButtonSize - height: elisaTheme.delegateToolButtonSize - } - - ToolButton { - id: openButton - - action: openAction - - width: elisaTheme.delegateToolButtonSize - height: elisaTheme.delegateToolButtonSize - } - - ToolButton { - id: enqueueAndPlayButton - - action: enqueueAndPlayAction - - width: elisaTheme.delegateToolButtonSize - height: elisaTheme.delegateToolButtonSize - } - } - } - - Image { - id: coverImage - - anchors.fill: parent - - sourceSize.width: parent.width - sourceSize.height: parent.height - fillMode: Image.PreserveAspectFit - smooth: true - - source: (mediaServerEntry.image ? mediaServerEntry.image : Qt.resolvedUrl(elisaTheme.defaultAlbumImage)) - - asynchronous: true - - layer.enabled: image === '' ? false : true - layer.effect: DropShadow { - source: coverImage - - radius: 10 - spread: 0.1 - samples: 21 - - color: myPalette.shadow - } - } - } - - LabelWithToolTip { - id: titleLabel - - font.weight: Font.Bold - color: myPalette.text - - horizontalAlignment: Text.AlignLeft - - Layout.topMargin: elisaTheme.layoutVerticalMargin * 0.5 - Layout.preferredWidth: mediaServerEntry.width * 0.85 - Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom - - elide: Text.ElideRight - } - - LabelWithToolTip { - id: artistLabel - - font.weight: Font.Light - color: myPalette.text - - horizontalAlignment: Text.AlignLeft - - Layout.preferredWidth: mediaServerEntry.width * 0.85 - Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom - - elide: Text.ElideRight - } - } - } - - Item { - Layout.fillHeight: true - } - } - - states: [ - State { - name: 'notSelected' - when: !mediaServerEntry.activeFocus && !hoverHandle.containsMouse - PropertyChanges { - target: hoverLoader - active: false - } - PropertyChanges { - target: hoverLoader - opacity: 0.0 - } - PropertyChanges { - target: coverImage - opacity: 1 - } - }, - State { - name: 'hoveredOrSelected' - when: mediaServerEntry.activeFocus || hoverHandle.containsMouse - PropertyChanges { - target: hoverLoader - active: true - } - PropertyChanges { - target: hoverLoader - opacity: 1.0 - } - PropertyChanges { - target: coverImage - opacity: 0.2 - } - } - ] - - transitions: [ - Transition { - to: 'hoveredOrSelected' - SequentialAnimation { - NumberAnimation { - properties: "active" - duration: 0 - } - NumberAnimation { - properties: "opacity" - easing.type: Easing.InOutQuad - duration: 100 - } - } - }, - Transition { - to: 'notSelected' - SequentialAnimation { - NumberAnimation { - properties: "opacity" - easing.type: Easing.InOutQuad - duration: 100 - } - NumberAnimation { - properties: "active" - duration: 0 - } - } - } - ] -} diff --git a/src/MediaAlbumView.qml b/src/MediaAlbumView.qml --- a/src/MediaAlbumView.qml +++ b/src/MediaAlbumView.qml @@ -28,22 +28,20 @@ FocusScope { id: topListing - property StackView stackView - property MediaPlayList playListModel property var musicListener - property var playerControl property var albumName property var artistName - property var tracksCount property var albumArtUrl property bool isSingleDiscAlbum - property var albumData property var albumId + property alias albumModel: contentDirectoryView.model signal showArtist(var name) - - width: stackView.width - height: stackView.height + signal enqueueAlbum(var album) + signal clearPlayList() + signal enqueueTrack(var track) + signal ensurePlay() + signal goBack(); SystemPalette { id: myPalette @@ -54,12 +52,6 @@ id: elisaTheme } - AlbumModel { - id: contentModel - - albumData: topListing.albumData - } - Connections { target: musicListener @@ -77,7 +69,7 @@ onAlbumModified: { if (albumId === modifiedAlbumId) { - albumData = modifiedAlbum + albumModel.albumData = modifiedAlbum contentModel.albumModified(modifiedAlbum) } } @@ -97,37 +89,22 @@ Layout.maximumHeight: height Layout.fillWidth: true - parentStackView: topListing.stackView - playList: topListing.playListModel - playerControl: topListing.playerControl - artist: topListing.artistName - album: topListing.albumName + mainTitle: (topListing.albumModel ? topListing.albumModel.author : '') + secondaryTitle: topListing.albumName image: (topListing.albumArtUrl ? topListing.albumArtUrl : elisaTheme.defaultAlbumImage) - tracksCount: topListing.tracksCount + allowArtistNavigation: true - enqueueAction: Action { - text: i18nc("Add whole album to play list", "Enqueue") - iconName: "media-track-add-amarok" - onTriggered: topListing.playListModel.enqueue(topListing.albumData) - } + onEnqueue: topListing.enqueueAlbum(albumModel.albumData) - clearAndEnqueueAction: Action { - text: i18nc("Clear play list and play", "Replace and Play") - tooltip: i18nc("Clear play list and add whole album to play list", "Replace Play List and Play Now") - iconName: "media-playback-start" - onTriggered: { - topListing.playListModel.clearAndEnqueue(topListing.albumData) - topListing.playerControl.ensurePlay() - } + onEnqueueAndPlay: { + topListing.clearPlayList() + topListing.enqueueAlbum(albumModel.albumData) + topListing.ensurePlay() } - navigateToArtistAction: Action { - text: i18nc("Button to navigate to the artist of the album", "Display Artist") - iconName: "view-media-artist" - onTriggered: { - showArtist(topListing.artistName) - } - } + onGoBack: topListing.goBack() + + onShowArtist: topListing.showArtist((topListing.albumModel ? topListing.albumModel.author : '')) } ScrollView { @@ -142,8 +119,6 @@ focus: true - model: contentModel - delegate: MediaAlbumTrackDelegate { id: entry @@ -196,11 +171,11 @@ true - mediaTrack.onClearPlaylist: topListing.playListModel.clearPlayList() + mediaTrack.onClearPlaylist: topListing.clearPlayList() - mediaTrack.onEnqueueToPlaylist: topListing.playListModel.enqueue(track) + mediaTrack.onEnqueueToPlaylist: topListing.enqueueTrack(track) - mediaTrack.onEnsurePlay: topListing.playerControl.ensurePlay() + mediaTrack.onEnsurePlay: topListing.ensurePlay() mediaTrack.onClicked: contentDirectoryView.currentIndex = index } diff --git a/src/MediaAllAlbumView.qml b/src/MediaAllAlbumView.qml deleted file mode 100644 --- a/src/MediaAllAlbumView.qml +++ /dev/null @@ -1,144 +0,0 @@ -/* - * 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.2 -import QtQuick.Controls.Styles 1.2 -import QtQuick.Window 2.2 -import QtQml.Models 2.1 -import QtQuick.Layouts 1.2 -import QtGraphicalEffects 1.0 - -import org.kde.elisa 1.0 - -FocusScope { - property var rootIndex - property StackView stackView - property MediaPlayList playListModel - property var musicListener - property var playerControl - property var contentDirectoryModel - - signal showArtist(var name) - - id: rootElement - - SystemPalette { - id: myPalette - colorGroup: SystemPalette.Active - } - - Theme { - id: elisaTheme - } - - ColumnLayout { - anchors.fill: parent - spacing: 0 - - FilterBar { - id: filterBar - labelText: i18nc("Title of the view of all albums", "Albums") - - height: elisaTheme.navigationBarHeight - Layout.preferredHeight: height - Layout.minimumHeight: height - Layout.maximumHeight: height - Layout.fillWidth: true - } - - Rectangle { - color: myPalette.base - - Layout.fillHeight: true - Layout.fillWidth: true - - ScrollView { - anchors.fill: parent - flickableItem.boundsBehavior: Flickable.StopAtBounds - flickableItem.interactive: true - - GridView { - id: contentDirectoryView - - focus: true - - TextMetrics { - id: textLineHeight - text: 'Album' - } - - cellWidth: elisaTheme.gridDelegateWidth - cellHeight: elisaTheme.gridDelegateWidth + elisaTheme.layoutVerticalMargin + textLineHeight.height * 2 - - model: DelegateModel { - id: delegateContentModel - - model: AlbumFilterProxyModel { - sourceModel: rootElement.contentDirectoryModel - - filterText: filterBar.filterText - - filterRating: filterBar.filterRating - } - - delegate: MediaAlbumDelegate { - width: contentDirectoryView.cellWidth - height: contentDirectoryView.cellHeight - - focus: true - - musicListener: rootElement.musicListener - image: if (model.image) - model.image - else - "" - title: if (model.title) - model.title - else - "" - artist: if (model.artist) - model.artist - else - "" - trackNumber: if (model.count !== undefined) - i18np("1 track", "%1 tracks", model.count) - else - i18nc("Number of tracks for an album", "0 track") - - isSingleDiscAlbum: model.isSingleDiscAlbum - - albumData: model.albumData - albumId: model.albumId - - stackView: rootElement.stackView - - playListModel: rootElement.playListModel - playerControl: rootElement.playerControl - - onAlbumClicked: contentDirectoryView.currentIndex = index - - onShowArtist: rootElement.showArtist(name) - } - } - } - } - } - } -} diff --git a/src/MediaAllArtistView.qml b/src/MediaAllArtistView.qml deleted file mode 100644 --- a/src/MediaAllArtistView.qml +++ /dev/null @@ -1,154 +0,0 @@ -/* - * 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.7 -import QtQuick.Controls 1.2 -import QtQuick.Controls.Styles 1.2 -import QtQuick.Window 2.2 -import QtQml.Models 2.1 -import QtQuick.Layouts 1.2 -import QtGraphicalEffects 1.0 - -import org.kde.elisa 1.0 - -FocusScope { - property var playerControl - property var playListModel - property var artistsModel - property var stackView - property var contentDirectoryModel - property var musicListener - - id: rootElement - - SystemPalette { - id: myPalette - colorGroup: SystemPalette.Active - } - - Theme { - id: elisaTheme - } - - ColumnLayout { - anchors.fill: parent - spacing: 0 - - FilterBar { - id: filterBar - labelText: i18nc("Title of the view of all artists", "Artists") - - showRating: false - height: elisaTheme.navigationBarHeight - Layout.preferredHeight: height - Layout.minimumHeight: height - Layout.maximumHeight: height - Layout.fillWidth: true - } - - Rectangle { - color: myPalette.base - - Layout.fillHeight: true - Layout.fillWidth: true - - ScrollView { - anchors.fill: parent - flickableItem.boundsBehavior: Flickable.StopAtBounds - flickableItem.interactive: true - - GridView { - id: contentDirectoryView - - focus: true - - TextMetrics { - id: textLineHeight - text: 'Artist' - } - - cellWidth: elisaTheme.gridDelegateWidth - cellHeight: elisaTheme.gridDelegateWidth + elisaTheme.layoutVerticalMargin + textLineHeight.height * 2 - - model: DelegateModel { - id: delegateContentModel - - model: SortFilterProxyModel { - sourceModel: artistsModel - - filterRole: AllArtistsModel.NameRole - - filterRegExp: new RegExp(filterBar.filterText, 'i') - } - - delegate: MediaArtistDelegate { - width: contentDirectoryView.cellWidth - height: contentDirectoryView.cellHeight - - focus: true - - musicListener: rootElement.musicListener - - image: if (model.image) - model.image - else - "" - - name: if (model.name) - model.name - else - "" - - stackView: rootElement.stackView - - playListModel: rootElement.playListModel - playerControl: rootElement.playerControl - contentDirectoryModel: rootElement.contentDirectoryModel - - onArtistClicked: contentDirectoryView.currentIndex = index - onOpenArtist: showArtistsAlbums(name) - } - } - } - } - } - } - - Component { - id: oneArtistView - - MediaArtistAlbumView { - playListModel: rootElement.playListModel - contentDirectoryModel: rootElement.contentDirectoryModel - playerControl: rootElement.playerControl - stackView: rootElement.stackView - musicListener: rootElement.musicListener - - onShowArtist: showArtistsAlbums(name) - } - } - - function showArtistsAlbums(name){ - if (stackView.depth === 3) { - stackView.pop() - } else { - stackView.push(oneArtistView, {artistName: name}) - } - } -} diff --git a/src/MediaArtistAlbumView.qml b/src/MediaArtistAlbumView.qml deleted file mode 100644 --- a/src/MediaArtistAlbumView.qml +++ /dev/null @@ -1,164 +0,0 @@ -/* - * 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.Window 2.2 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.2 -import QtQml.Models 2.1 -import QtGraphicalEffects 1.0 - -import org.kde.elisa 1.0 - -Item { - property var rootIndex - property StackView stackView - property MediaPlayList playListModel - property var musicListener - property var playerControl - property var contentDirectoryModel - - property alias artistName: navBar.artist - - signal showArtist(var name) - - id: rootElement - - SystemPalette { - id: myPalette - colorGroup: SystemPalette.Active - } - - Theme { - id: elisaTheme - } - - ColumnLayout { - anchors.fill: parent - spacing: 0 - - NavigationActionBar { - id: navBar - - height: elisaTheme.navigationBarHeight - - Layout.preferredHeight: height - Layout.minimumHeight: height - Layout.maximumHeight: height - Layout.fillWidth: true - - parentStackView: rootElement.stackView - playList: rootElement.playListModel - playerControl: rootElement.playerControl - image: Qt.resolvedUrl(elisaTheme.defaultArtistImage) - - enqueueAction: Action { - text: i18nc("Add all tracks from artist to play list", "Enqueue") - iconName: "media-track-add-amarok" - onTriggered: rootElement.playListModel.enqueue(rootElement.artistName) - } - - clearAndEnqueueAction: Action { - text: i18nc("Clear play list and play", "Replace and Play") - tooltip: i18nc("Clear play list and add all tracks from artist to play list", "Replace Play List and Play Now") - iconName: "media-playback-start" - onTriggered: { - rootElement.playListModel.clearAndEnqueue(rootElement.artistName) - rootElement.playerControl.ensurePlay() - } - } - } - - Rectangle { - color: myPalette.base - - Layout.fillHeight: true - Layout.fillWidth: true - - ScrollView { - anchors.fill: parent - flickableItem.boundsBehavior: Flickable.StopAtBounds - flickableItem.interactive: true - - GridView { - id: contentDirectoryView - - TextMetrics { - id: textLineHeight - text: 'Album' - } - - cellWidth: elisaTheme.gridDelegateWidth - cellHeight: elisaTheme.gridDelegateWidth + elisaTheme.layoutVerticalMargin * 3 + textLineHeight.height * 2 - - model: DelegateModel { - id: delegateContentModel - - model: SortFilterProxyModel { - sourceModel: contentDirectoryModel - filterRole: AllAlbumsModel.AllArtistsRole - - filterRegExp: new RegExp('.*' + artistName + '.*', 'i') - } - - delegate: MediaAlbumDelegate { - width: contentDirectoryView.cellWidth - height: contentDirectoryView.cellHeight - - musicListener: rootElement.musicListener - image: if (model.image) - model.image - else - "" - title: if (model.title) - model.title - else - "" - artist: if (model.artist) - model.artist - else - "" - trackNumber: if (model.count !== undefined) - i18np("1 track", "%1 tracks", model.count) - else - i18nc("Number of tracks for an album", "0 track") - - isSingleDiscAlbum: model.isSingleDiscAlbum - - albumData: model.albumData - albumId: model.albumId - - stackView: rootElement.stackView - - playListModel: rootElement.playListModel - playerControl: rootElement.playerControl - - onAlbumClicked: contentDirectoryView.currentIndex = index - - onShowArtist: rootElement.showArtist(name) - } - } - - focus: true - } - } - } - } -} diff --git a/src/MediaBrowser.qml b/src/MediaBrowser.qml --- a/src/MediaBrowser.qml +++ b/src/MediaBrowser.qml @@ -28,7 +28,7 @@ id: contentDirectoryRoot property MediaPlayList playListModel - property var firstPage + property alias firstPage: listingView.initialItem property alias stackView: listingView function goBack() { @@ -70,8 +70,6 @@ } } } - - initialItem: firstPage } } diff --git a/src/MediaServer.qml b/src/MediaServer.qml --- a/src/MediaServer.qml +++ b/src/MediaServer.qml @@ -80,15 +80,15 @@ } - Action { - text: goBackAction.text - shortcut: goBackAction.shortcut - iconName: elisa.iconName(goBackAction.icon) - onTriggered: { - localAlbums.goBack() - localArtists.goBack() - } - } + Action { + text: goBackAction.text + shortcut: goBackAction.shortcut + iconName: elisa.iconName(goBackAction.icon) + onTriggered: { + localAlbums.goBack() + localArtists.goBack() + } + } Action { id: qmlQuitAction @@ -142,7 +142,7 @@ id: allListeners elisaApplication: elisa - } + } AudioWrapper { id: audioPlayer @@ -265,6 +265,14 @@ AllAlbumsModel { id: allAlbumsModel + + allArtists: allArtistsModel + } + + AllArtistsModel { + id: allArtistsModel + + allAlbums: allAlbumsModel } AllTracksModel { @@ -312,10 +320,6 @@ onTrackModified: allTracksModel.trackModified(modifiedTrack) } - AllArtistsModel { - id: allArtistsModel - } - Connections { target: allListeners @@ -594,18 +598,34 @@ rightMargin: elisaTheme.layoutHorizontalMargin } - firstPage: MediaAllAlbumView { + firstPage: GridBrowserView { + id: allAlbumsView + focus: true - playListModel: playListModelItem - playerControl: manageAudioPlayer - stackView: localAlbums.stackView - musicListener: allListeners - contentDirectoryModel: allAlbumsModel - onShowArtist: { - listViews.currentIndex = 2 - allArtistsView.showArtistsAlbums(name) + isFirstPage: true + + model: AlbumFilterProxyModel { + sourceModel: allAlbumsModel } + + mainTitle: i18nc("Title of the view of all albums", "Albums") + + onEnqueue: playListModelItem.enqueue(data) + onEnqueueAndPlay: { + playListModelItem.clearAndEnqueue(data) + manageAudioPlayer.ensurePlay() + } + onOpen: { + localAlbums.stackView.push(albumView, { + stackView: localAlbums.stackView, + albumName: innerMainTitle, + albumModel: innerModel, + albumArtUrl: innerImage, + musicListener: allListeners + }) + } + onGoBack: localAlbums.stackView.pop() } visible: opacity > 0 @@ -623,16 +643,35 @@ rightMargin: elisaTheme.layoutHorizontalMargin } - firstPage: MediaAllArtistView { + firstPage: GridBrowserView { id: allArtistsView - focus: true - playListModel: playListModelItem - artistsModel: allArtistsModel - playerControl: manageAudioPlayer - stackView: localArtists.stackView - musicListener: allListeners - contentDirectoryModel: allAlbumsModel + + isFirstPage: true + showRating: false + delegateDisplaySecondaryText: false + + model: AlbumFilterProxyModel { + sourceModel: allArtistsModel + } + + mainTitle: i18nc("Title of the view of all artists", "Artists") + + onEnqueue: playListModelItem.enqueue(data) + onEnqueueAndPlay: { + playListModelItem.clearAndEnqueue(data) + manageAudioPlayer.ensurePlay() + } + onOpen: { + localArtists.stackView.push(innerAlbumView, { + model: innerModel, + mainTitle: innerMainTitle, + secondaryTitle: innerSecondaryTitle, + image: innerImage, + stackView: localArtists.stackView + }) + } + onGoBack: localArtists.stackView.pop() } visible: opacity > 0 @@ -970,4 +1009,57 @@ } } } + + Component { + id: innerAlbumView + + GridBrowserView { + property var stackView + + onEnqueue: playListModelItem.enqueue(data) + onEnqueueAndPlay: { + playListModelItem.clearAndEnqueue(data) + manageAudioPlayer.ensurePlay() + } + onOpen: { + localArtists.stackView.push(albumView, { + stackView: localArtists.stackView, + albumName: innerMainTitle, + albumModel: innerModel, + musicListener: allListeners + }) + } + onGoBack: stackView.pop() + } + } + + Component { + id: albumView + + MediaAlbumView { + property var stackView + + onEnsurePlay: manageAudioPlayer.ensurePlay() + onClearPlayList: playListModelItem.clearPlayList() + onEnqueueAlbum: playListModelItem.enqueue(album) + onEnqueueTrack: playListModelItem.enqueue(track) + onShowArtist: { + listViews.currentIndex = 2 + if (localArtists.stackView.depth === 3) { + localArtists.stackView.pop() + } + if (localArtists.stackView.depth === 2) { + var artistPage = localArtists.stackView.get(1) + if (artistPage.mainTitle === name) { + return + } else { + localArtists.stackView.pop() + } + } + + allArtistsView.open(allArtistsModel.itemModelForName(name), name, '', elisaTheme.defaultArtistImage) + } + onGoBack: stackView.pop() + } + } } diff --git a/src/NavigationActionBar.qml b/src/NavigationActionBar.qml --- a/src/NavigationActionBar.qml +++ b/src/NavigationActionBar.qml @@ -26,25 +26,21 @@ Item { id: navigationBar - property var parentStackView - property var playList - property var playerControl - property string artist - property string album - property string image - property string tracksCount - property var enqueueAction - property var clearAndEnqueueAction - property var navigateToArtistAction: Action { } + property string mainTitle + property string secondaryTitle + property url image + property bool allowArtistNavigation: false + + signal enqueue(); + signal enqueueAndPlay(); + signal goBack(); + signal showArtist(); Action { id: goPreviousAction text: i18nc("navigate back in the views stack", "Back") iconName: (Qt.application.layoutDirection == Qt.RightToLeft) ? "go-next" : "go-previous" - onTriggered: - { - parentStackView.pop() - } + onTriggered: goBack() } RowLayout { @@ -105,7 +101,7 @@ LabelWithToolTip { id: albumLabel - text: album + text: secondaryTitle Layout.fillWidth: true Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter @@ -118,7 +114,7 @@ pixelSize: elisaTheme.defaultFontPixelSize * 1.5 } - visible: album !== "" + visible: secondaryTitle !== "" } TextMetrics { @@ -131,15 +127,15 @@ LabelWithToolTip { id: authorLabel - text: artist + text: mainTitle color: myPalette.text Layout.fillWidth: true Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter font { - pixelSize: (album !== "" ? elisaTheme.defaultFontPixelSize : elisaTheme.defaultFontPixelSize * 1.5) + pixelSize: (secondaryTitle !== "" ? elisaTheme.defaultFontPixelSize : elisaTheme.defaultFontPixelSize * 1.5) } elide: Text.ElideRight @@ -157,26 +153,37 @@ spacing: 0 Button { - action: enqueueAction + text: i18nc("Add current list to playlist", "Enqueue") + iconName: "media-track-add-amarok" + + onClicked: enqueue() Layout.leftMargin: 0 Layout.rightMargin: 0 } Button { - action: clearAndEnqueueAction + text: i18nc("Clear playlist and play", "Replace and Play") + tooltip: i18nc("Clear playlist and add current list to it", "Replace PlayList and Play Now") + iconName: "media-playback-start" + + onClicked: enqueueAndPlay() Layout.leftMargin: !LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 Layout.rightMargin: LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 } Button { - action: navigateToArtistAction + id: showArtistButton + + visible: allowArtistNavigation + text: i18nc("Button to navigate to the artist of the album", "Display Artist") + iconName: "view-media-artist" + + onClicked: showArtist() Layout.leftMargin: !LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 Layout.rightMargin: LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 - - visible: album !== "" } } } diff --git a/src/albumfilterproxymodel.cpp b/src/albumfilterproxymodel.cpp --- a/src/albumfilterproxymodel.cpp +++ b/src/albumfilterproxymodel.cpp @@ -20,6 +20,7 @@ #include "albumfilterproxymodel.h" #include "allalbumsmodel.h" +#include "allartistsmodel.h" AlbumFilterProxyModel::AlbumFilterProxyModel(QObject *parent) : QSortFilterProxyModel(parent), mFilterText() { diff --git a/src/albummodel.h b/src/albummodel.h --- a/src/albummodel.h +++ b/src/albummodel.h @@ -46,14 +46,16 @@ Q_PROPERTY(QString title READ title - WRITE setTitle NOTIFY titleChanged) Q_PROPERTY(QString author READ author - WRITE setAuthor NOTIFY authorChanged) + Q_PROPERTY(int tracksCount + READ tracksCount + NOTIFY tracksCountChanged) + public: enum ItemClass { @@ -81,6 +83,9 @@ DiscFirstTrackRole, IsSingleDiscAlbumRole, TrackDataRole, + SecondaryTextRole, + ImageUrlRole, + ShadowForImageRole, }; Q_ENUM(ColumnsRoles) @@ -109,22 +114,22 @@ QString author() const; + int tracksCount() const; + Q_SIGNALS: void albumDataChanged(); void titleChanged(); void authorChanged(); + void tracksCountChanged(); + public Q_SLOTS: void setAlbumData(const MusicAlbum &album); - void setTitle(const QString &title); - - void setAuthor(const QString &author); - void albumModified(const MusicAlbum &modifiedAlbum); void albumRemoved(const MusicAlbum &modifiedAlbum); diff --git a/src/albummodel.cpp b/src/albummodel.cpp --- a/src/albummodel.cpp +++ b/src/albummodel.cpp @@ -34,10 +34,6 @@ { } - QString mTitle; - - QString mAuthor; - MusicAlbum mCurrentAlbum; }; @@ -76,6 +72,9 @@ roles[static_cast(ColumnsRoles::IsSingleDiscAlbumRole)] = "isSingleDiscAlbum"; roles[static_cast(ColumnsRoles::TrackDataRole)] = "trackData"; roles[static_cast(ColumnsRoles::ResourceRole)] = "trackResource"; + roles[static_cast(ColumnsRoles::SecondaryTextRole)] = "secondaryText"; + roles[static_cast(ColumnsRoles::ImageUrlRole)] = "imageUrl"; + roles[static_cast(ColumnsRoles::ShadowForImageRole)] = "shadowForImage"; return roles; } @@ -128,9 +127,7 @@ { auto result = QVariant(); - ColumnsRoles convertedRole = static_cast(role); - - switch(convertedRole) + switch(role) { case ColumnsRoles::TitleRole: result = track.title(); @@ -201,6 +198,42 @@ case ColumnsRoles::TrackDataRole: result = QVariant::fromValue(track); break; + case Qt::DisplayRole: + result = track.title(); + break; + case ColumnsRoles::SecondaryTextRole: + { + auto secondaryText = QString(); + secondaryText = QStringLiteral("%1 - %2%3"); + + secondaryText = secondaryText.arg(track.trackNumber()); + secondaryText = secondaryText.arg(track.title()); + + if (track.artist() == track.albumArtist()) { + secondaryText = secondaryText.arg(QString()); + } else { + auto artistText = QString(); + artistText = QStringLiteral(" - %1"); + artistText = artistText.arg(track.artist()); + secondaryText = secondaryText.arg(artistText); + } + + result = secondaryText; + break; + } + case ColumnsRoles::ImageUrlRole: + { + const auto &albumArtUri = d->mCurrentAlbum.albumArtURI(); + if (albumArtUri.isValid()) { + result = albumArtUri; + } else { + result = QUrl(QStringLiteral("image://icon/media-optical-audio")); + } + break; + } + case ColumnsRoles::ShadowForImageRole: + result = d->mCurrentAlbum.albumArtURI().isValid(); + break; } return result; @@ -245,12 +278,17 @@ QString AlbumModel::title() const { - return d->mTitle; + return d->mCurrentAlbum.title(); } QString AlbumModel::author() const { - return d->mAuthor; + return d->mCurrentAlbum.artist(); +} + +int AlbumModel::tracksCount() const +{ + return d->mCurrentAlbum.tracksCount(); } void AlbumModel::setAlbumData(const MusicAlbum &album) @@ -270,24 +308,9 @@ endInsertRows(); Q_EMIT albumDataChanged(); -} - -void AlbumModel::setTitle(const QString &title) -{ - if (d->mTitle == title) - return; - - d->mTitle = title; - emit titleChanged(); -} - -void AlbumModel::setAuthor(const QString &author) -{ - if (d->mAuthor == author) - return; - - d->mAuthor = author; - emit authorChanged(); + Q_EMIT tracksCountChanged(); + Q_EMIT authorChanged(); + Q_EMIT titleChanged(); } void AlbumModel::albumModified(const MusicAlbum &modifiedAlbum) diff --git a/src/allalbumsmodel.h b/src/allalbumsmodel.h --- a/src/allalbumsmodel.h +++ b/src/allalbumsmodel.h @@ -32,7 +32,7 @@ class AllAlbumsModelPrivate; class MusicStatistics; -class QMutex; +class AllArtistsModel; class AllAlbumsModel : public QAbstractItemModel { @@ -42,6 +42,11 @@ READ albumCount NOTIFY albumCountChanged) + Q_PROPERTY(AllArtistsModel* allArtists + READ allArtists + WRITE setAllArtists + NOTIFY allArtistsChanged) + public: enum ColumnsRoles { @@ -56,6 +61,12 @@ AlbumDataRole, HighestTrackRating, AlbumDatabaseIdRole, + SecondaryTextRole, + ImageUrlRole, + ShadowForImageRole, + ContainerDataRole, + ChildModelRole, + IsTracksContainerRole, }; Q_ENUM(ColumnsRoles) @@ -80,18 +91,24 @@ int columnCount(const QModelIndex &parent = QModelIndex()) const override; + AllArtistsModel *allArtists() const; + public Q_SLOTS: void albumAdded(const MusicAlbum &newAlbum); void albumRemoved(const MusicAlbum &removedAlbum); void albumModified(const MusicAlbum &modifiedAlbum); + void setAllArtists(AllArtistsModel *model); + Q_SIGNALS: void albumCountChanged(); + void allArtistsChanged(); + private: QVariant internalDataAlbum(int albumIndex, int role) const; diff --git a/src/allalbumsmodel.cpp b/src/allalbumsmodel.cpp --- a/src/allalbumsmodel.cpp +++ b/src/allalbumsmodel.cpp @@ -20,6 +20,8 @@ #include "allalbumsmodel.h" #include "musicstatistics.h" #include "databaseinterface.h" +#include "allartistsmodel.h" +#include "albummodel.h" #include #include @@ -40,6 +42,8 @@ int mAlbumCount = 0; + AllArtistsModel *mAllArtistsModel = nullptr; + }; AllAlbumsModel::AllAlbumsModel(QObject *parent) : QAbstractItemModel(parent), d(std::make_unique()) @@ -81,6 +85,12 @@ roles[static_cast(ColumnsRoles::AlbumDataRole)] = "albumData"; roles[static_cast(ColumnsRoles::HighestTrackRating)] = "highestTrackRating"; roles[static_cast(ColumnsRoles::AlbumDatabaseIdRole)] = "albumId"; + roles[static_cast(ColumnsRoles::SecondaryTextRole)] = "secondaryText"; + roles[static_cast(ColumnsRoles::ImageUrlRole)] = "imageUrl"; + roles[static_cast(ColumnsRoles::ShadowForImageRole)] = "shadowForImage"; + roles[static_cast(ColumnsRoles::ContainerDataRole)] = "containerData"; + roles[static_cast(ColumnsRoles::ChildModelRole)] = "childModel"; + roles[static_cast(ColumnsRoles::IsTracksContainerRole)] = "isTracksContainer"; return roles; } @@ -132,9 +142,7 @@ { auto result = QVariant(); - ColumnsRoles convertedRole = static_cast(role); - - switch(convertedRole) + switch(role) { case ColumnsRoles::TitleRole: result = d->mAllAlbums[albumIndex].title(); @@ -174,6 +182,38 @@ case ColumnsRoles::HighestTrackRating: result = d->mAllAlbums[albumIndex].highestTrackRating(); break; + case Qt::DisplayRole: + result = d->mAllAlbums[albumIndex].title(); + break; + case ColumnsRoles::SecondaryTextRole: + result = d->mAllAlbums[albumIndex].artist(); + break; + case ColumnsRoles::ImageUrlRole: + { + auto albumArt = d->mAllAlbums[albumIndex].albumArtURI(); + if (albumArt.isValid()) { + result = albumArt; + } else { + result = QUrl(QStringLiteral("image://icon/media-optical-audio")); + } + break; + } + case ColumnsRoles::ShadowForImageRole: + result = d->mAllAlbums[albumIndex].albumArtURI().isValid(); + break; + case ColumnsRoles::ContainerDataRole: + result = QVariant::fromValue(d->mAllAlbums[albumIndex]); + break; + case ColumnsRoles::ChildModelRole: + { + auto newModel = new AlbumModel(); + newModel->setAlbumData(d->mAllAlbums[albumIndex]); + result = QVariant::fromValue(newModel); + break; + } + case ColumnsRoles::IsTracksContainerRole: + result = true; + break; } return result; @@ -212,6 +252,11 @@ return 1; } +AllArtistsModel *AllAlbumsModel::allArtists() const +{ + return d->mAllArtistsModel; +} + void AllAlbumsModel::albumAdded(const MusicAlbum &newAlbum) { if (newAlbum.isValid()) { @@ -256,4 +301,14 @@ Q_EMIT dataChanged(index(albumIndex, 0), index(albumIndex, 0)); } +void AllAlbumsModel::setAllArtists(AllArtistsModel *model) +{ + if (d->mAllArtistsModel == model) { + return; + } + + d->mAllArtistsModel = model; + Q_EMIT allArtistsChanged(); +} + #include "moc_allalbumsmodel.cpp" diff --git a/src/allartistsmodel.h b/src/allartistsmodel.h --- a/src/allartistsmodel.h +++ b/src/allartistsmodel.h @@ -31,18 +31,30 @@ class DatabaseInterface; class AllArtistsModelPrivate; +class AllAlbumsModel; class AllArtistsModel : public QAbstractItemModel { Q_OBJECT + Q_PROPERTY(AllAlbumsModel* allAlbums + READ allAlbums + WRITE setAllAlbums + NOTIFY allAlbumsChanged) + public: enum ColumnsRoles { NameRole = Qt::UserRole + 1, ArtistsCountRole, ImageRole, IdRole, + SecondaryTextRole, + ImageUrlRole, + ShadowForImageRole, + ContainerDataRole, + ChildModelRole, + IsTracksContainerRole, }; Q_ENUM(ColumnsRoles) @@ -65,14 +77,24 @@ int columnCount(const QModelIndex &parent = QModelIndex()) const override; + AllAlbumsModel* allAlbums() const; + + Q_INVOKABLE QAbstractItemModel* itemModelForName(const QString &name) const; + +Q_SIGNALS: + + void allAlbumsChanged(); + public Q_SLOTS: void artistAdded(const MusicArtist &newArtist); void artistRemoved(const MusicArtist &removedArtist); void artistModified(const MusicArtist &modifiedArtist); + void setAllAlbums(AllAlbumsModel *model); + private: std::unique_ptr d; diff --git a/src/allartistsmodel.cpp b/src/allartistsmodel.cpp --- a/src/allartistsmodel.cpp +++ b/src/allartistsmodel.cpp @@ -20,11 +20,13 @@ #include "allartistsmodel.h" #include "databaseinterface.h" #include "musicartist.h" +#include "allalbumsmodel.h" #include #include #include #include +#include class AllArtistsModelPrivate { @@ -40,6 +42,8 @@ bool mUseLocalIcons = false; + AllAlbumsModel *mAllAlbumsModel = nullptr; + }; AllArtistsModel::AllArtistsModel(QObject *parent) : QAbstractItemModel(parent), d(std::make_unique()) @@ -70,6 +74,12 @@ roles[static_cast(ColumnsRoles::ArtistsCountRole)] = "albumsCount"; roles[static_cast(ColumnsRoles::ImageRole)] = "image"; roles[static_cast(ColumnsRoles::IdRole)] = "id"; + roles[static_cast(ColumnsRoles::SecondaryTextRole)] = "secondaryText"; + roles[static_cast(ColumnsRoles::ImageUrlRole)] = "imageUrl"; + roles[static_cast(ColumnsRoles::ShadowForImageRole)] = "shadowForImage"; + roles[static_cast(ColumnsRoles::ContainerDataRole)] = "containerData"; + roles[static_cast(ColumnsRoles::ChildModelRole)] = "childModel"; + roles[static_cast(ColumnsRoles::IsTracksContainerRole)] = "isTracksContainer"; return roles; } @@ -113,9 +123,7 @@ return result; } - ColumnsRoles convertedRole = static_cast(role); - - switch(convertedRole) + switch(role) { case ColumnsRoles::NameRole: result = d->mAllArtists[index.row()].name(); @@ -127,6 +135,34 @@ break; case ColumnsRoles::IdRole: break; + case ColumnsRoles::SecondaryTextRole: + result = QString(); + break; + case Qt::DisplayRole: + result = d->mAllArtists[index.row()].name(); + break; + case ColumnsRoles::ImageUrlRole: + result = QUrl(QStringLiteral("image://icon/view-media-artist")); + break; + case ColumnsRoles::ShadowForImageRole: + result = false; + break; + case ColumnsRoles::ContainerDataRole: + result = QVariant::fromValue(d->mAllArtists[index.row()]); + break; + case ColumnsRoles::ChildModelRole: + { + auto newModel = new QSortFilterProxyModel; + newModel->setSourceModel(d->mAllAlbumsModel); + newModel->setFilterRole(AllAlbumsModel::AllArtistsRole); + newModel->setFilterRegExp(QRegExp(QStringLiteral(".*") + d->mAllArtists[index.row()].name() + QStringLiteral(".*"), + Qt::CaseInsensitive)); + result = QVariant::fromValue(newModel); + break; + } + case ColumnsRoles::IsTracksContainerRole: + result = false; + break; } return result; @@ -165,6 +201,22 @@ return 1; } +AllAlbumsModel *AllArtistsModel::allAlbums() const +{ + return d->mAllAlbumsModel; +} + +QAbstractItemModel *AllArtistsModel::itemModelForName(const QString &name) const +{ + auto newModel = new QSortFilterProxyModel; + newModel->setSourceModel(d->mAllAlbumsModel); + newModel->setFilterRole(AllAlbumsModel::AllArtistsRole); + newModel->setFilterRegExp(QRegExp(QStringLiteral(".*") + name + QStringLiteral(".*"), + Qt::CaseInsensitive)); + + return newModel; +} + void AllArtistsModel::artistAdded(const MusicArtist &newArtist) { if (newArtist.isValid()) { @@ -196,4 +248,15 @@ Q_UNUSED(modifiedArtist); } +void AllArtistsModel::setAllAlbums(AllAlbumsModel *model) +{ + if (d->mAllAlbumsModel == model) { + return; + } + + d->mAllAlbumsModel = model; + + Q_EMIT allAlbumsChanged(); +} + #include "moc_allartistsmodel.cpp" diff --git a/src/alltracksmodel.h b/src/alltracksmodel.h --- a/src/alltracksmodel.h +++ b/src/alltracksmodel.h @@ -51,6 +51,9 @@ DatabaseIdRole, IsSingleDiscAlbumRole, TrackDataRole, + SecondaryTextRole, + ImageUrlRole, + ShadowForImageRole, }; Q_ENUM(ColumnsRoles) diff --git a/src/alltracksmodel.cpp b/src/alltracksmodel.cpp --- a/src/alltracksmodel.cpp +++ b/src/alltracksmodel.cpp @@ -70,6 +70,9 @@ roles[static_cast(ColumnsRoles::IsSingleDiscAlbumRole)] = "isSingleDiscAlbum"; roles[static_cast(ColumnsRoles::TrackDataRole)] = "trackData"; roles[static_cast(ColumnsRoles::ResourceRole)] = "trackResource"; + roles[static_cast(ColumnsRoles::SecondaryTextRole)] = "secondaryText"; + roles[static_cast(ColumnsRoles::ImageUrlRole)] = "imageUrl"; + roles[static_cast(ColumnsRoles::ShadowForImageRole)] = "shadowForImage"; return roles; } @@ -113,9 +116,9 @@ return result; } - ColumnsRoles convertedRole = static_cast(role); + const auto &track = d->mAllTracks[d->mIds[index.row()]]; - switch(convertedRole) + switch(role) { case ColumnsRoles::TitleRole: if (d->mAllTracks[d->mIds[index.row()]].title().isEmpty()) { @@ -185,6 +188,42 @@ case ColumnsRoles::TrackDataRole: result = QVariant::fromValue(d->mAllTracks[d->mIds[index.row()]]); break; + case Qt::DisplayRole: + result = track.title(); + break; + case ColumnsRoles::SecondaryTextRole: + { + auto secondaryText = QString(); + secondaryText = QStringLiteral("%1 - %2%3"); + + secondaryText = secondaryText.arg(track.trackNumber()); + secondaryText = secondaryText.arg(track.title()); + + if (track.artist() == track.albumArtist()) { + secondaryText = secondaryText.arg(QString()); + } else { + auto artistText = QString(); + artistText = QStringLiteral(" - %1"); + artistText = artistText.arg(track.artist()); + secondaryText = secondaryText.arg(artistText); + } + + result = secondaryText; + break; + } + case ColumnsRoles::ImageUrlRole: + { + const auto &imageUrl = d->mAllTracks[d->mIds[index.row()]].albumCover(); + if (imageUrl.isValid()) { + result = imageUrl; + } else { + result = QUrl(QStringLiteral("image://icon/media-optical-audio")); + } + break; + } + case ColumnsRoles::ShadowForImageRole: + result = d->mAllTracks[d->mIds[index.row()]].albumCover().isValid(); + break; } return result; diff --git a/src/mediaplaylist.h b/src/mediaplaylist.h --- a/src/mediaplaylist.h +++ b/src/mediaplaylist.h @@ -91,6 +91,9 @@ IsPlayingRole, HasAlbumHeader, IsSingleDiscAlbumHeader, + SecondaryTextRole, + ImageUrlRole, + ShadowForImageRole, }; Q_ENUM(ColumnsRoles) diff --git a/src/mediaplaylist.cpp b/src/mediaplaylist.cpp --- a/src/mediaplaylist.cpp +++ b/src/mediaplaylist.cpp @@ -85,14 +85,8 @@ return result; } - if (role < ColumnsRoles::IsValidRole || role > ColumnsRoles::IsSingleDiscAlbumHeader) { - return result; - } - - ColumnsRoles convertedRole = static_cast(role); - if (d->mData[index.row()].mIsValid) { - switch(convertedRole) + switch(role) { case ColumnsRoles::IsValidRole: result = d->mData[index.row()].mIsValid; @@ -170,9 +164,43 @@ case ColumnsRoles::IsPlayingRole: result = d->mData[index.row()].mIsPlaying; break; + case Qt::DisplayRole: + { + const auto &track = d->mTrackData[index.row()]; + auto displayText = QString(); + displayText = QStringLiteral("%1 - %2"); + + if (track.isSingleDiscAlbum()) { + displayText = displayText.arg(track.trackNumber()); + } else { + auto numbersText = QString(); + numbersText = QStringLiteral("%1 - %2"); + numbersText = numbersText.arg(track.discNumber()); + numbersText = numbersText.arg(track.trackNumber()); + displayText = displayText.arg(numbersText); + } + + result = displayText.arg(track.title()); + break; + } + case ColumnsRoles::SecondaryTextRole: + break; + case ColumnsRoles::ImageUrlRole: + { + const auto &albumArt = d->mTrackData[index.row()].albumCover(); + if (albumArt.isValid()) { + result = albumArt; + } else { + result = QUrl(QStringLiteral("image://icon/media-optical-audio")); + } + break; + } + case ColumnsRoles::ShadowForImageRole: + result = d->mTrackData[index.row()].albumCover().isValid(); + break; } } else { - switch(convertedRole) + switch(role) { case ColumnsRoles::IsValidRole: result = d->mData[index.row()].mIsValid; @@ -226,6 +254,18 @@ case ColumnsRoles::ImageRole: result = QStringLiteral(""); break; + case Qt::DisplayRole: + result = d->mTrackData[index.row()].title(); + break; + case ColumnsRoles::SecondaryTextRole: + result = QString(); + break; + case ColumnsRoles::ImageUrlRole: + result = QUrl(QStringLiteral("image://icon/error")); + break; + case ColumnsRoles::ShadowForImageRole: + result = false; + break; } } @@ -290,6 +330,9 @@ roles[static_cast(ColumnsRoles::IsPlayingRole)] = "isPlaying"; roles[static_cast(ColumnsRoles::HasAlbumHeader)] = "hasAlbumHeader"; roles[static_cast(ColumnsRoles::IsSingleDiscAlbumHeader)] = "isSingleDiscAlbum"; + roles[static_cast(ColumnsRoles::SecondaryTextRole)] = "secondaryText"; + roles[static_cast(ColumnsRoles::ImageUrlRole)] = "imageUrl"; + roles[static_cast(ColumnsRoles::ShadowForImageRole)] = "shadowForImage"; return roles; } diff --git a/src/upnpControl.qrc b/src/upnpControl.qrc --- a/src/upnpControl.qrc +++ b/src/upnpControl.qrc @@ -13,11 +13,6 @@ NavigationActionBar.qml PlayListEntry.qml MediaBrowser.qml - MediaAlbumDelegate.qml - MediaArtistDelegate.qml - MediaAllAlbumView.qml - MediaArtistAlbumView.qml - MediaAllArtistView.qml Theme.qml PlatformIntegration.qml LabelWithToolTip.qml @@ -29,6 +24,8 @@ FilterBar.qml MediaTrackDelegate.qml MediaAlbumTrackDelegate.qml + GridBrowserView.qml + GridBrowserDelegate.qml windows/WindowsTheme.qml