Add FeedListPage sorting

The feeds will be sorted by (1) descending number of unread/unplayed
entries and (2) alphabetically by name.  The current item and current
selection are maintained after re-sorting when the number of unread
entries has changed.
This commit is contained in:
Bart De Vries 2021-09-20 16:57:43 +02:00
parent c84d8ed47f
commit bd1cf2c5f0
12 changed files with 112 additions and 38 deletions

View File

@ -17,6 +17,7 @@ set(SRCS_base
storagemovejob.cpp
models/chaptermodel.cpp
models/feedsmodel.cpp
models/feedsproxymodel.cpp
models/entriesmodel.cpp
models/queuemodel.cpp
models/episodemodel.cpp

View File

@ -45,7 +45,7 @@
#include "models/episodemodel.h"
#include "models/episodeproxymodel.h"
#include "models/errorlogmodel.h"
#include "models/feedsmodel.h"
#include "models/feedsproxymodel.h"
#include "models/podcastsearchmodel.h"
#include "models/queuemodel.h"
#include "mpris2/mpris2.h"
@ -120,7 +120,7 @@ int main(int argc, char *argv[])
engine.rootContext()->setContextProperty(QStringLiteral("_aboutData"), QVariant::fromValue(about));
qmlRegisterType<FeedsModel>("org.kde.kasts", 1, 0, "FeedsModel");
qmlRegisterType<FeedsProxyModel>("org.kde.kasts", 1, 0, "FeedsProxyModel");
qmlRegisterType<QueueModel>("org.kde.kasts", 1, 0, "QueueModel");
qmlRegisterType<EpisodeProxyModel>("org.kde.kasts", 1, 0, "EpisodeProxyModel");
qmlRegisterType<Mpris2>("org.kde.kasts", 1, 0, "Mpris2");
@ -130,6 +130,7 @@ int main(int argc, char *argv[])
qmlRegisterUncreatableType<EntriesModel>("org.kde.kasts", 1, 0, "EntriesModel", QStringLiteral("Get from Feed"));
qmlRegisterUncreatableType<Enclosure>("org.kde.kasts", 1, 0, "Enclosure", QStringLiteral("Only for enums"));
qmlRegisterUncreatableType<EpisodeModel>("org.kde.kasts", 1, 0, "EpisodeModel", QStringLiteral("Only for enums"));
qmlRegisterUncreatableType<FeedsModel>("org.kde.kasts", 1, 0, "FeedsModel", QStringLiteral("Only for enums"));
qmlRegisterSingletonInstance("org.kde.kasts", 1, 0, "Fetcher", &Fetcher::instance());
qmlRegisterSingletonInstance("org.kde.kasts", 1, 0, "Database", &Database::instance());

View File

@ -12,8 +12,8 @@
#include "datamanager.h"
#include "entry.h"
EpisodeModel::EpisodeModel()
: QAbstractListModel(nullptr)
EpisodeModel::EpisodeModel(QObject *parent)
: QAbstractListModel(parent)
{
// When feed is updated, the entire model needs to be reset because we
// cannot know where the new entries will be inserted into the list (or that

View File

@ -25,7 +25,7 @@ public:
};
Q_ENUM(Roles)
explicit EpisodeModel();
explicit EpisodeModel(QObject *parent = nullptr);
QVariant data(const QModelIndex &index, int role = Qt::UserRole) const override;
QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex &parent) const override;

View File

@ -11,19 +11,14 @@
#include "datamanager.h"
#include "entry.h"
EpisodeProxyModel::EpisodeProxyModel()
: QSortFilterProxyModel(nullptr)
EpisodeProxyModel::EpisodeProxyModel(QObject *parent)
: QSortFilterProxyModel(parent)
{
m_currentFilter = NoFilter;
m_episodeModel = new EpisodeModel();
m_episodeModel = new EpisodeModel(this);
setSourceModel(m_episodeModel);
}
EpisodeProxyModel::~EpisodeProxyModel()
{
delete m_episodeModel;
}
bool EpisodeProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);

View File

@ -30,8 +30,7 @@ public:
Q_PROPERTY(FilterType filterType READ filterType WRITE setFilterType NOTIFY filterTypeChanged)
Q_PROPERTY(QString filterName READ filterName NOTIFY filterTypeChanged)
explicit EpisodeProxyModel();
~EpisodeProxyModel();
explicit EpisodeProxyModel(QObject *parent = nullptr);
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;

View File

@ -28,6 +28,14 @@ FeedsModel::FeedsModel(QObject *parent)
beginRemoveRows(QModelIndex(), index, index);
endRemoveRows();
});
connect(&DataManager::instance(), &DataManager::unreadEntryCountChanged, this, [=](const QString &url) {
for (int i = 0; i < rowCount(QModelIndex()); i++) {
if (data(index(i, 0), UrlRole).toString() == url) {
Q_EMIT dataChanged(index(i, 0), index(i, 0));
return;
}
}
});
}
QHash<int, QByteArray> FeedsModel::roleNames() const
@ -61,9 +69,3 @@ QVariant FeedsModel::data(const QModelIndex &index, int role) const
return QVariant();
}
}
// Hack to get a QItemSelection in QML
QItemSelection FeedsModel::createSelection(int rowa, int rowb)
{
return QItemSelection(index(rowa, 0), index(rowb, 0));
}

View File

@ -9,7 +9,6 @@
#include <QAbstractListModel>
#include <QHash>
#include <QItemSelection>
#include <QSqlTableModel>
#include <QUrl>
@ -32,6 +31,4 @@ public:
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex &parent) const override;
Q_INVOKABLE QItemSelection createSelection(int rowa, int rowb);
};

View File

@ -0,0 +1,36 @@
/**
* SPDX-FileCopyrightText: 2021 Bart De Vries <bart@mogwai.be>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
#include "models/feedsproxymodel.h"
FeedsProxyModel::FeedsProxyModel(QObject *parent)
: QSortFilterProxyModel(parent)
{
m_feedsModel = new FeedsModel(this);
setSourceModel(m_feedsModel);
setDynamicSortFilter(true);
sort(0, Qt::AscendingOrder);
}
bool FeedsProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
{
QString leftTitle = sourceModel()->data(left, FeedsModel::TitleRole).toString();
QString rightTitle = sourceModel()->data(right, FeedsModel::TitleRole).toString();
int leftUnreadCount = sourceModel()->data(left, FeedsModel::UnreadCountRole).toInt();
int rightUnreadCount = sourceModel()->data(right, FeedsModel::UnreadCountRole).toInt();
if (leftUnreadCount == rightUnreadCount) {
return QString::localeAwareCompare(leftTitle, rightTitle) < 0;
} else {
return leftUnreadCount > rightUnreadCount;
}
}
// Hack to get a QItemSelection in QML
QItemSelection FeedsProxyModel::createSelection(int rowa, int rowb)
{
return QItemSelection(index(rowa, 0), index(rowb, 0));
}

View File

@ -0,0 +1,29 @@
/**
* SPDX-FileCopyrightText: 2021 Bart De Vries <bart@mogwai.be>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
#pragma once
#include <QItemSelection>
#include <QSortFilterProxyModel>
#include "models/feedsmodel.h"
class Entry;
class FeedsProxyModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
explicit FeedsProxyModel(QObject *parent = nullptr);
bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
Q_INVOKABLE QItemSelection createSelection(int rowa, int rowb);
private:
FeedsModel *m_feedsModel;
};

View File

@ -18,14 +18,16 @@ import org.kde.kasts 1.0
Controls.ItemDelegate {
id: feedDelegate
required property int cardSize
required property int cardMargin
property int cardSize: 0
property int cardMargin: 0
property int borderWidth: 1
implicitWidth: cardSize + 2 * cardMargin
implicitHeight: cardSize + 2 * cardMargin
property QtObject listView: undefined
property bool selected: false
property bool isCurrentItem: false // to restore currentItem when model is resorted
property string currentItemUrl: ""
property int row: model ? model.row : -1
property var activeBackgroundColor: Qt.lighter(Kirigami.Theme.highlightColor, 1.3)
highlighted: selected
@ -135,8 +137,26 @@ Controls.ItemDelegate {
Connections {
target: listView.model
function onLayoutAboutToBeChanged() {
if (feedList.currentItem === feedDelegate) {
isCurrentItem = true;
currentItemUrl = feed.url;
} else {
isCurrentItem = false;
currentItemUrl = "";
}
}
function onLayoutChanged() {
updateIsSelected();
if (isCurrentItem) {
// yet another hack because "index" is still giving the old
// value here; so we have to manually find the new index.
for (var i = 0; i < feedList.model.rowCount(); i++) {
if (feedList.model.data(feedList.model.index(i, 0), FeedsModel.UrlRole) == currentItemUrl) {
feedList.currentIndex = i;
}
}
}
}
}

View File

@ -115,21 +115,14 @@ Kirigami.ScrollablePage {
cellWidth: availableWidth / columns
cellHeight: availableWidth / columns
model: FeedsModel {
model: FeedsProxyModel {
id: feedsModel
}
Component {
id: feedListDelegate
FeedListDelegate {
cardSize: feedList.availableWidth / feedList.columns - 2 * feedList.cardMargin
cardMargin: feedList.cardMargin
listView: feedList
}
}
delegate: Kirigami.DelegateRecycler {
sourceComponent: feedListDelegate
delegate: FeedListDelegate {
cardSize: feedList.availableWidth / feedList.columns - 2 * feedList.cardMargin
cardMargin: feedList.cardMargin
listView: feedList
}
property var selectionForContextMenu: []
@ -153,6 +146,7 @@ Kirigami.ScrollablePage {
currentIndex = 0;
}
}
Keys.onPressed: {
if (event.matches(StandardKey.SelectAll)) {
feedList.selectionModel.select(model.index(0, 0), ItemSelectionModel.ClearAndSelect | ItemSelectionModel.Columns);