Commit fc9736e1 authored by Matthieu Gallien's avatar Matthieu Gallien 🎵
Browse files

use ListView sections and section headers for album headers

should help avoid playlist corruption especially when loading m3u
playlist files

CCBUG: 398093
parent 8efa5789
This diff is collapsed.
......@@ -31,7 +31,6 @@ Item {
width: 300
isAlternateColor: false
hasAlbumHeader: false
isSingleDiscAlbum: false
title: "hello"
isValid: true
......
......@@ -337,6 +337,7 @@ if (Qt5Quick_FOUND AND Qt5Widgets_FOUND)
qml/PlayListEntry.qml
qml/SimplePlayListView.qml
qml/SimplePlayListEntry.qml
qml/PlayListAlbumHeader.qml
qml/MediaTrackDelegate.qml
qml/MediaAlbumTrackDelegate.qml
......
......@@ -25,6 +25,8 @@
#include <QList>
#include <QMediaPlaylist>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonDocument>
#include <QDebug>
#include <algorithm>
......@@ -101,7 +103,6 @@ QHash<int, QByteArray> MediaPlayList::roleNames() const
roles[static_cast<int>(ColumnsRoles::SampleRateRole)] = "sampleRate";
roles[static_cast<int>(ColumnsRoles::CountRole)] = "count";
roles[static_cast<int>(ColumnsRoles::IsPlayingRole)] = "isPlaying";
roles[static_cast<int>(ColumnsRoles::HasAlbumHeader)] = "hasAlbumHeader";
roles[static_cast<int>(ColumnsRoles::IsSingleDiscAlbumRole)] = "isSingleDiscAlbum";
roles[static_cast<int>(ColumnsRoles::SecondaryTextRole)] = "secondaryText";
roles[static_cast<int>(ColumnsRoles::ImageUrlRole)] = "imageUrl";
......@@ -109,6 +110,7 @@ QHash<int, QByteArray> MediaPlayList::roleNames() const
roles[static_cast<int>(ColumnsRoles::ResourceRole)] = "trackResource";
roles[static_cast<int>(ColumnsRoles::TrackDataRole)] = "trackData";
roles[static_cast<int>(ColumnsRoles::AlbumIdRole)] = "albumId";
roles[static_cast<int>(ColumnsRoles::AlbumSectionRole)] = "albumSection";
return roles;
}
......@@ -123,9 +125,6 @@ QVariant MediaPlayList::data(const QModelIndex &index, int role) const
case ColumnsRoles::IsValidRole:
result = d->mData[index.row()].mIsValid;
break;
case ColumnsRoles::HasAlbumHeader:
result = rowHasHeader(index.row());
break;
case ColumnsRoles::IsPlayingRole:
result = d->mData[index.row()].mIsPlaying;
break;
......@@ -139,6 +138,11 @@ QVariant MediaPlayList::data(const QModelIndex &index, int role) const
}
break;
}
case ColumnsRoles::AlbumSectionRole:
result = QJsonDocument{QJsonArray{d->mTrackData[index.row()][TrackDataType::key_type::AlbumRole].toString(),
d->mTrackData[index.row()][TrackDataType::key_type::AlbumArtistRole].toString(),
d->mTrackData[index.row()][TrackDataType::key_type::ImageUrlRole].toUrl().toString()}}.toJson();
break;
default:
result = d->mTrackData[index.row()][static_cast<TrackDataType::key_type>(role)];
}
......@@ -166,9 +170,6 @@ QVariant MediaPlayList::data(const QModelIndex &index, int role) const
case ColumnsRoles::TrackNumberRole:
result = -1;
break;
case ColumnsRoles::HasAlbumHeader:
result = rowHasHeader(index.row());
break;
case ColumnsRoles::IsSingleDiscAlbumRole:
result = false;
break;
......@@ -181,6 +182,11 @@ QVariant MediaPlayList::data(const QModelIndex &index, int role) const
case ColumnsRoles::ShadowForImageRole:
result = false;
break;
case ColumnsRoles::AlbumSectionRole:
result = QJsonDocument{QJsonArray{d->mData[index.row()].mAlbum.toString(),
d->mData[index.row()].mArtist.toString(),
QUrl(QStringLiteral("image://icon/error")).toString()}}.toJson();
break;
default:
result = {};
}
......@@ -201,7 +207,7 @@ bool MediaPlayList::setData(const QModelIndex &index, const QVariant &value, int
return modelModified;
}
if (role < ColumnsRoles::IsValidRole || role > ColumnsRoles::HasAlbumHeader) {
if (role < ColumnsRoles::IsValidRole || role > ColumnsRoles::IsPlayingRole) {
return modelModified;
}
......@@ -233,12 +239,6 @@ bool MediaPlayList::removeRows(int row, int count, const QModelIndex &parent)
{
beginRemoveRows(parent, row, row + count - 1);
bool hadAlbumHeader = false;
if (rowCount() > row + count) {
hadAlbumHeader = rowHasHeader(row + count);
}
for (int i = row, cpt = 0; cpt < count; ++i, ++cpt) {
d->mData.removeAt(i);
d->mTrackData.removeAt(i);
......@@ -266,15 +266,6 @@ bool MediaPlayList::removeRows(int row, int count, const QModelIndex &parent)
}
Q_EMIT tracksCountChanged();
if (hadAlbumHeader != rowHasHeader(row)) {
Q_EMIT dataChanged(index(row, 0), index(row, 0), {ColumnsRoles::HasAlbumHeader});
if (!d->mCurrentTrack.isValid()) {
resetCurrentTrack();
}
}
Q_EMIT persistentStateChanged();
return false;
......@@ -290,13 +281,6 @@ bool MediaPlayList::moveRows(const QModelIndex &sourceParent, int sourceRow, int
return false;
}
auto firstMovedTrackHasHeader = rowHasHeader(sourceRow);
auto nextTrackHasHeader = rowHasHeader(sourceRow + count);
auto futureNextTrackHasHeader = rowHasHeader(destinationChild);
if (sourceRow < destinationChild) {
nextTrackHasHeader = rowHasHeader(sourceRow + count);
}
for (auto cptItem = 0; cptItem < count; ++cptItem) {
if (sourceRow < destinationChild) {
d->mData.move(sourceRow, destinationChild - 1);
......@@ -309,60 +293,6 @@ bool MediaPlayList::moveRows(const QModelIndex &sourceParent, int sourceRow, int
endMoveRows();
if (sourceRow < destinationChild) {
if (firstMovedTrackHasHeader != rowHasHeader(destinationChild - count)) {
Q_EMIT dataChanged(index(destinationChild - count, 0), index(destinationChild - count, 0), {ColumnsRoles::HasAlbumHeader});
if (!d->mCurrentTrack.isValid()) {
resetCurrentTrack();
}
}
} else {
if (firstMovedTrackHasHeader != rowHasHeader(destinationChild)) {
Q_EMIT dataChanged(index(destinationChild, 0), index(destinationChild, 0), {ColumnsRoles::HasAlbumHeader});
if (!d->mCurrentTrack.isValid()) {
resetCurrentTrack();
}
}
}
if (sourceRow < destinationChild) {
if (nextTrackHasHeader != rowHasHeader(sourceRow)) {
Q_EMIT dataChanged(index(sourceRow, 0), index(sourceRow, 0), {ColumnsRoles::HasAlbumHeader});
if (!d->mCurrentTrack.isValid()) {
resetCurrentTrack();
}
}
} else {
if (nextTrackHasHeader != rowHasHeader(sourceRow + count)) {
Q_EMIT dataChanged(index(sourceRow + count, 0), index(sourceRow + count, 0), {ColumnsRoles::HasAlbumHeader});
if (!d->mCurrentTrack.isValid()) {
resetCurrentTrack();
}
}
}
if (sourceRow < destinationChild) {
if (futureNextTrackHasHeader != rowHasHeader(destinationChild + count - 1)) {
Q_EMIT dataChanged(index(destinationChild + count - 1, 0), index(destinationChild + count - 1, 0), {ColumnsRoles::HasAlbumHeader});
if (!d->mCurrentTrack.isValid()) {
resetCurrentTrack();
}
}
} else {
if (futureNextTrackHasHeader != rowHasHeader(destinationChild + count)) {
Q_EMIT dataChanged(index(destinationChild + count, 0), index(destinationChild + count, 0), {ColumnsRoles::HasAlbumHeader});
if (!d->mCurrentTrack.isValid()) {
resetCurrentTrack();
}
}
}
Q_EMIT persistentStateChanged();
return true;
......@@ -415,7 +345,7 @@ void MediaPlayList::enqueueRestoredEntry(const MediaPlayListEntry &newEntry)
}
if (!newEntry.mIsValid) {
Q_EMIT dataChanged(index(rowCount() - 1, 0), index(rowCount() - 1, 0), {MediaPlayList::HasAlbumHeader});
Q_EMIT dataChanged(index(rowCount() - 1, 0), index(rowCount() - 1, 0), {MediaPlayList::IsPlayingRole});
if (!d->mCurrentTrack.isValid()) {
resetCurrentTrack();
......@@ -469,7 +399,7 @@ void MediaPlayList::enqueueFilesList(const ElisaUtils::EntryDataList &newEntries
Q_EMIT tracksCountChanged();
Q_EMIT persistentStateChanged();
Q_EMIT dataChanged(index(rowCount() - 1, 0), index(rowCount() - 1, 0), {MediaPlayList::HasAlbumHeader});
Q_EMIT dataChanged(index(rowCount() - 1, 0), index(rowCount() - 1, 0), {MediaPlayList::IsPlayingRole});
}
void MediaPlayList::enqueueTracksListById(const ElisaUtils::EntryDataList &newEntries)
......@@ -490,7 +420,7 @@ void MediaPlayList::enqueueTracksListById(const ElisaUtils::EntryDataList &newEn
Q_EMIT tracksCountChanged();
Q_EMIT persistentStateChanged();
Q_EMIT dataChanged(index(rowCount() - 1, 0), index(rowCount() - 1, 0), {MediaPlayList::HasAlbumHeader});
Q_EMIT dataChanged(index(rowCount() - 1, 0), index(rowCount() - 1, 0), {MediaPlayList::IsPlayingRole});
}
void MediaPlayList::enqueueOneEntry(const ElisaUtils::EntryData &entryData, ElisaUtils::PlayListEntryType type)
......@@ -1038,47 +968,6 @@ void MediaPlayList::trackInError(const QUrl &sourceInError, QMediaPlayer::Error
}
}
bool MediaPlayList::rowHasHeader(int row) const
{
if (row >= rowCount()) {
return false;
}
if (row < 0) {
return false;
}
if (row - 1 < 0) {
return true;
}
auto currentAlbumTitle = QString();
auto currentAlbumArtist = QString();
if (d->mData[row].mIsValid) {
currentAlbumTitle = d->mTrackData[row].album();
currentAlbumArtist = d->mTrackData[row].albumArtist();
} else {
currentAlbumTitle = d->mData[row].mAlbum.toString();
currentAlbumArtist = d->mData[row].mArtist.toString();
}
auto previousAlbumTitle = QString();
auto previousAlbumArtist = QString();
if (d->mData[row - 1].mIsValid) {
previousAlbumTitle = d->mTrackData[row - 1].album();
previousAlbumArtist = d->mTrackData[row - 1].albumArtist();
} else {
previousAlbumTitle = d->mData[row - 1].mAlbum.toString();
previousAlbumArtist = d->mData[row - 1].mArtist.toString();
}
if (currentAlbumTitle == previousAlbumTitle && currentAlbumArtist == previousAlbumArtist) {
return false;
}
return true;
}
void MediaPlayList::loadPlayListLoaded()
{
clearPlayList();
......
......@@ -117,7 +117,7 @@ public:
TrackDataRole,
CountRole,
IsPlayingRole,
HasAlbumHeader,
AlbumSectionRole,
};
Q_ENUM(ColumnsRoles)
......@@ -264,8 +264,6 @@ private Q_SLOTS:
private:
bool rowHasHeader(int row) const;
void resetCurrentTrack();
void notifyCurrentTrackChanged();
......
......@@ -69,6 +69,9 @@ Item {
elisaTheme.layoutVerticalMargin * 5 +
playListAuthorTextHeight.height +
playListAlbumTextHeight.height
property int playListHeaderHeight: elisaTheme.layoutVerticalMargin * 5 +
playListAuthorTextHeight.height +
playListAlbumTextHeight.height
property int trackDelegateHeight: dp(45)
......
/*
* Copyright 2016-2017 Matthieu Gallien <matthieu_gallien@yahoo.fr>
*
* This program is free software: you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import QtQuick 2.7
import QtQuick.Layouts 1.2
import QtQuick.Controls 2.3
import org.kde.elisa 1.0
import QtQuick 2.0
Rectangle {
property var headerData
property string album: headerData[0]
property string albumArtist: headerData[1]
property url imageUrl: headerData[2]
color: myPalette.midlight
TextMetrics {
id: trackNumberSize
text: (99).toLocaleString(Qt.locale(), 'f', 0)
}
TextMetrics {
id: fakeDiscNumberSize
text: '/9'
}
RowLayout {
id: headerRow
spacing: elisaTheme.layoutHorizontalMargin
anchors.fill: parent
anchors.topMargin: elisaTheme.layoutVerticalMargin * 1.5
anchors.bottomMargin: elisaTheme.layoutVerticalMargin * 1.5
Image {
id: mainIcon
source: (imageUrl != '' ? imageUrl : Qt.resolvedUrl(elisaTheme.defaultAlbumImage))
Layout.minimumWidth: headerRow.height
Layout.maximumWidth: headerRow.height
Layout.preferredWidth: headerRow.height
Layout.minimumHeight: headerRow.height
Layout.maximumHeight: headerRow.height
Layout.preferredHeight: headerRow.height
Layout.leftMargin: !LayoutMirroring.enabled ?
(elisaTheme.smallDelegateToolButtonSize +
trackNumberSize.width +
fakeDiscNumberSize.width +
(elisaTheme.layoutHorizontalMargin * 5 / 4) -
headerRow.height) :
0
Layout.rightMargin: LayoutMirroring.enabled ?
(elisaTheme.smallDelegateToolButtonSize +
trackNumberSize.width +
fakeDiscNumberSize.width +
(elisaTheme.layoutHorizontalMargin * 5 / 4) -
headerRow.height) :
0
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
sourceSize.width: headerRow.height
sourceSize.height: headerRow.height
fillMode: Image.PreserveAspectFit
asynchronous: true
opacity: 1
}
ColumnLayout {
id: albumHeaderTextColumn
Layout.fillWidth: true
Layout.fillHeight: true
Layout.leftMargin: !LayoutMirroring.enabled ? - elisaTheme.layoutHorizontalMargin / 4 : 0
Layout.rightMargin: LayoutMirroring.enabled ? - elisaTheme.layoutHorizontalMargin / 4 : 0
spacing: 0
LabelWithToolTip {
id: mainLabel
text: album
font.weight: Font.Bold
font.pointSize: elisaTheme.defaultFontPointSize * 1.4
color: myPalette.text
horizontalAlignment: Text.AlignLeft
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
Layout.topMargin: elisaTheme.layoutVerticalMargin
elide: Text.ElideRight
}
Item {
Layout.fillHeight: true
}
LabelWithToolTip {
id: authorLabel
text: albumArtist
font.weight: Font.Light
color: myPalette.text
horizontalAlignment: Text.AlignLeft
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
Layout.bottomMargin: elisaTheme.layoutVerticalMargin
elide: Text.ElideRight
}
}
}
}
......@@ -34,6 +34,15 @@ ListView {
activeFocusOnTab: true
keyNavigationEnabled: true
section.property: 'albumSection'
section.criteria: ViewSection.FullString
section.labelPositioning: ViewSection.InlineLabels
section.delegate: PlayListAlbumHeader {
headerData: JSON.parse(section)
width: scrollBar.visible ? (!LayoutMirroring.enabled ? playListView.width - scrollBar.width : playListView.width) : playListView.width
height: elisaTheme.playListHeaderHeight
}
ScrollBar.vertical: ScrollBar {
id: scrollBar
}
......@@ -114,7 +123,6 @@ ListView {
trackNumber: model.trackNumber
discNumber: model.discNumber
rating: model.rating
hasAlbumHeader: model.hasAlbumHeader
isSingleDiscAlbum: model.isSingleDiscAlbum
isValid: model.isValid
isPlaying: model.isPlaying
......
......@@ -44,7 +44,6 @@ FocusScope {
property int trackNumber
property int discNumber
property int rating
property bool hasAlbumHeader
property bool hasValidDiscNumber: true
property int scrollBarWidth
property bool noBackground: false
......@@ -54,7 +53,7 @@ FocusScope {
signal removeFromPlaylist(var trackIndex)
signal switchToTrack(var trackIndex)
height: (hasAlbumHeader ? elisaTheme.playListDelegateWithHeaderHeight : elisaTheme.playListDelegateHeight)
height: elisaTheme.playListDelegateHeight
Controls1.Action {
id: removeFromPlayList
......@@ -122,7 +121,7 @@ FocusScope {
color: (isAlternateColor ? myPalette.alternateBase : myPalette.base)
height: (hasAlbumHeader ? elisaTheme.playListDelegateWithHeaderHeight : elisaTheme.playListDelegateHeight)
height: elisaTheme.playListDelegateHeight
focus: true
......@@ -131,118 +130,6 @@ FocusScope {
anchors.fill: parent
Loader {
Layout.fillWidth: true
Layout.preferredHeight: elisaTheme.playListDelegateWithHeaderHeight - elisaTheme.playListDelegateHeight
Layout.minimumHeight: elisaTheme.playListDelegateWithHeaderHeight - elisaTheme.playListDelegateHeight
Layout.maximumHeight: elisaTheme.playListDelegateWithHeaderHeight - elisaTheme.playListDelegateHeight
visible: hasAlbumHeader
active: hasAlbumHeader
sourceComponent: Rectangle {
color: myPalette.midlight
anchors.fill: parent
RowLayout {
id: headerRow
spacing: elisaTheme.layoutHorizontalMargin
anchors.fill: parent
anchors.topMargin: elisaTheme.layoutVerticalMargin * 1.5
anchors.bottomMargin: elisaTheme.layoutVerticalMargin * 1.5
Image {
id: mainIcon
source: (isValid ? (imageUrl != '' ? imageUrl : Qt.resolvedUrl(elisaTheme.defaultAlbumImage)) : Qt.resolvedUrl(elisaTheme.errorIcon))
Layout.minimumWidth: headerRow.height
Layout.maximumWidth: headerRow.height
Layout.preferredWidth: headerRow.height
Layout.minimumHeight: headerRow.height
Layout.maximumHeight: headerRow.height
Layout.preferredHeight: headerRow.height
Layout.leftMargin: !LayoutMirroring.enabled ?
(elisaTheme.smallDelegateToolButtonSize +
trackNumberSize.width +
fakeDiscNumberSize.width +
(elisaTheme.layoutHorizontalMargin * 5 / 4) -
headerRow.height) :
0
Layout.rightMargin: LayoutMirroring.enabled ?
(elisaTheme.smallDelegateToolButtonSize +
trackNumberSize.width +
fakeDiscNumberSize.width +
(elisaTheme.layoutHorizontalMargin * 5 / 4) -
headerRow.height) :
0
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
sourceSize.width: headerRow.height
sourceSize.height: headerRow.height
fillMode: Image.PreserveAspectFit
asynchronous: true
opacity: isValid ? 1 : 0.5
}
ColumnLayout {
id: albumHeaderTextColumn
Layout.fillWidth: true
Layout.fillHeight: true
Layout.leftMargin: !LayoutMirroring.enabled ? - elisaTheme.layoutHorizontalMargin / 4 : 0
Layout.rightMargin: LayoutMirroring.enabled ? - elisaTheme.layoutHorizontalMargin / 4 : 0
spacing: 0
LabelWithToolTip {
id: mainLabel
text: album
font.weight: Font.Bold
font.pointSize: elisaTheme.defaultFontPointSize * 1.4
color: myPalette.text
horizontalAlignment: Text.AlignLeft
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
Layout.topMargin: elisaTheme.layoutVerticalMargin
elide: Text.ElideRight
}
Item {
Layout.fillHeight: true
}
LabelWithToolTip {
id: authorLabel
text: albumArtist
font.weight: Font.Light
color: myPalette.text
horizontalAlignment: Text.AlignLeft
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
Layout.bottomMargin: elisaTheme.layoutVerticalMargin
elide: Text.ElideRight
}
}
}
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
......
......@@ -40,6 +40,7 @@
<file>qml/SimplePlayListView.qml</file>
<file>qml/SimplePlayListEntry.qml</file>
<file>qml/ViewSelector.qml</file>
<file>qml/PlayListAlbumHeader.qml</file>
</qresource>
<qresource prefix="/qml/+windows">
<file alias="Theme.qml">windows/WindowsTheme.qml</file>
......