Implement backend to allow Feed list sort and search
The current list of things to sort on (ascending and descending), includes: - unplayed episodes - new episodes - favorite episodes - title (alphabetical) For the first three categories, the value of the sort quantity will be shown in the upper right corner of the delegate. BUG: 471012 CCBUG: 459885
This commit is contained in:
parent
4b1fe5e3f9
commit
c3ca038af7
@ -267,6 +267,8 @@ if(ANDROID)
|
|||||||
view-sort
|
view-sort
|
||||||
view-sort-descending
|
view-sort-descending
|
||||||
view-sort-ascending
|
view-sort-ascending
|
||||||
|
view-sort-descending-name
|
||||||
|
view-sort-ascending-name
|
||||||
)
|
)
|
||||||
else()
|
else()
|
||||||
target_link_libraries(kasts PRIVATE Qt::Widgets)
|
target_link_libraries(kasts PRIVATE Qt::Widgets)
|
||||||
|
@ -196,28 +196,6 @@ int DataManager::entryCount(const Feed *feed) const
|
|||||||
return m_entrymap[feed->url()].count();
|
return m_entrymap[feed->url()].count();
|
||||||
}
|
}
|
||||||
|
|
||||||
int DataManager::newEntryCount(const Feed *feed) const
|
|
||||||
{
|
|
||||||
QSqlQuery query;
|
|
||||||
query.prepare(QStringLiteral("SELECT COUNT (id) FROM Entries where feed=:feed AND new=1;"));
|
|
||||||
query.bindValue(QStringLiteral(":feed"), feed->url());
|
|
||||||
Database::instance().execute(query);
|
|
||||||
if (!query.next())
|
|
||||||
return -1;
|
|
||||||
return query.value(0).toInt();
|
|
||||||
}
|
|
||||||
|
|
||||||
int DataManager::favoriteEntryCount(const Feed *feed) const
|
|
||||||
{
|
|
||||||
QSqlQuery query;
|
|
||||||
query.prepare(QStringLiteral("SELECT COUNT (id) FROM Entries where feed=:feed AND favorite=1;"));
|
|
||||||
query.bindValue(QStringLiteral(":feed"), feed->url());
|
|
||||||
Database::instance().execute(query);
|
|
||||||
if (!query.next())
|
|
||||||
return -1;
|
|
||||||
return query.value(0).toInt();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DataManager::removeFeed(Feed *feed)
|
void DataManager::removeFeed(Feed *feed)
|
||||||
{
|
{
|
||||||
QList<Feed *> feeds;
|
QList<Feed *> feeds;
|
||||||
|
@ -37,8 +37,6 @@ public:
|
|||||||
QStringList getIdList(const Feed *feed) const;
|
QStringList getIdList(const Feed *feed) const;
|
||||||
int entryCount(const int feed_index) const;
|
int entryCount(const int feed_index) const;
|
||||||
int entryCount(const Feed *feed) const;
|
int entryCount(const Feed *feed) const;
|
||||||
int newEntryCount(const Feed *feed) const;
|
|
||||||
int favoriteEntryCount(const Feed *feed) const;
|
|
||||||
Q_INVOKABLE void addFeed(const QString &url);
|
Q_INVOKABLE void addFeed(const QString &url);
|
||||||
void addFeed(const QString &url, const bool fetch);
|
void addFeed(const QString &url, const bool fetch);
|
||||||
void addFeeds(const QStringList &urls);
|
void addFeeds(const QStringList &urls);
|
||||||
|
43
src/feed.cpp
43
src/feed.cpp
@ -44,6 +44,8 @@ Feed::Feed(const QString &feedurl)
|
|||||||
|
|
||||||
updateAuthors();
|
updateAuthors();
|
||||||
updateUnreadEntryCountFromDB();
|
updateUnreadEntryCountFromDB();
|
||||||
|
updateNewEntryCountFromDB();
|
||||||
|
updateFavoriteEntryCountFromDB();
|
||||||
|
|
||||||
connect(&Fetcher::instance(), &Fetcher::feedUpdateStatusChanged, this, [this](const QString &url, bool status) {
|
connect(&Fetcher::instance(), &Fetcher::feedUpdateStatusChanged, this, [this](const QString &url, bool status) {
|
||||||
if (url == m_url) {
|
if (url == m_url) {
|
||||||
@ -54,11 +56,26 @@ Feed::Feed(const QString &feedurl)
|
|||||||
if (url == m_url) {
|
if (url == m_url) {
|
||||||
Q_EMIT entryCountChanged();
|
Q_EMIT entryCountChanged();
|
||||||
updateUnreadEntryCountFromDB();
|
updateUnreadEntryCountFromDB();
|
||||||
|
Q_EMIT DataManager::instance().unreadEntryCountChanged(m_url);
|
||||||
Q_EMIT unreadEntryCountChanged();
|
Q_EMIT unreadEntryCountChanged();
|
||||||
|
Q_EMIT DataManager::instance().newEntryCountChanged(m_url);
|
||||||
|
Q_EMIT newEntryCountChanged();
|
||||||
setErrorId(0);
|
setErrorId(0);
|
||||||
setErrorString(QLatin1String(""));
|
setErrorString(QLatin1String(""));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
connect(&DataManager::instance(), &DataManager::newEntryCountChanged, this, [this](const QString &url) {
|
||||||
|
if (url == m_url) {
|
||||||
|
updateNewEntryCountFromDB();
|
||||||
|
Q_EMIT newEntryCountChanged();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connect(&DataManager::instance(), &DataManager::favoriteEntryCountChanged, this, [this](const QString &url) {
|
||||||
|
if (url == m_url) {
|
||||||
|
updateFavoriteEntryCountFromDB();
|
||||||
|
Q_EMIT favoriteEntryCountChanged();
|
||||||
|
}
|
||||||
|
});
|
||||||
connect(&Fetcher::instance(),
|
connect(&Fetcher::instance(),
|
||||||
&Fetcher::error,
|
&Fetcher::error,
|
||||||
this,
|
this,
|
||||||
@ -141,6 +158,28 @@ void Feed::updateUnreadEntryCountFromDB()
|
|||||||
m_unreadEntryCount = query.value(0).toInt();
|
m_unreadEntryCount = query.value(0).toInt();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Feed::updateNewEntryCountFromDB()
|
||||||
|
{
|
||||||
|
QSqlQuery query;
|
||||||
|
query.prepare(QStringLiteral("SELECT COUNT (id) FROM Entries where feed=:feed AND new=1;"));
|
||||||
|
query.bindValue(QStringLiteral(":feed"), m_url);
|
||||||
|
Database::instance().execute(query);
|
||||||
|
if (!query.next())
|
||||||
|
m_newEntryCount = -1;
|
||||||
|
m_newEntryCount = query.value(0).toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Feed::updateFavoriteEntryCountFromDB()
|
||||||
|
{
|
||||||
|
QSqlQuery query;
|
||||||
|
query.prepare(QStringLiteral("SELECT COUNT (id) FROM Entries where feed=:feed AND favorite=1;"));
|
||||||
|
query.bindValue(QStringLiteral(":feed"), m_url);
|
||||||
|
Database::instance().execute(query);
|
||||||
|
if (!query.next())
|
||||||
|
m_favoriteEntryCount = -1;
|
||||||
|
m_favoriteEntryCount = query.value(0).toInt();
|
||||||
|
}
|
||||||
|
|
||||||
QString Feed::url() const
|
QString Feed::url() const
|
||||||
{
|
{
|
||||||
return m_url;
|
return m_url;
|
||||||
@ -218,12 +257,12 @@ int Feed::unreadEntryCount() const
|
|||||||
|
|
||||||
int Feed::newEntryCount() const
|
int Feed::newEntryCount() const
|
||||||
{
|
{
|
||||||
return DataManager::instance().newEntryCount(this);
|
return m_newEntryCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
int Feed::favoriteEntryCount() const
|
int Feed::favoriteEntryCount() const
|
||||||
{
|
{
|
||||||
return DataManager::instance().favoriteEntryCount(this);
|
return m_favoriteEntryCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Feed::refreshing() const
|
bool Feed::refreshing() const
|
||||||
|
@ -111,6 +111,8 @@ Q_SIGNALS:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
void updateUnreadEntryCountFromDB();
|
void updateUnreadEntryCountFromDB();
|
||||||
|
void updateNewEntryCountFromDB();
|
||||||
|
void updateFavoriteEntryCountFromDB();
|
||||||
|
|
||||||
QString m_url;
|
QString m_url;
|
||||||
QString m_name;
|
QString m_name;
|
||||||
@ -127,6 +129,8 @@ private:
|
|||||||
int m_errorId;
|
int m_errorId;
|
||||||
QString m_errorString;
|
QString m_errorString;
|
||||||
int m_unreadEntryCount = -1;
|
int m_unreadEntryCount = -1;
|
||||||
|
int m_newEntryCount = -1;
|
||||||
|
int m_favoriteEntryCount = -1;
|
||||||
|
|
||||||
EntriesProxyModel *m_entries;
|
EntriesProxyModel *m_entries;
|
||||||
|
|
||||||
|
@ -28,22 +28,10 @@ FeedsModel::FeedsModel(QObject *parent)
|
|||||||
beginRemoveRows(QModelIndex(), index, index);
|
beginRemoveRows(QModelIndex(), index, index);
|
||||||
endRemoveRows();
|
endRemoveRows();
|
||||||
});
|
});
|
||||||
connect(&DataManager::instance(), &DataManager::unreadEntryCountChanged, this, [=](const QString &url) {
|
connect(&DataManager::instance(), &DataManager::unreadEntryCountChanged, this, &FeedsModel::triggerFeedUpdate);
|
||||||
for (int i = 0; i < rowCount(QModelIndex()); i++) {
|
connect(&DataManager::instance(), &DataManager::newEntryCountChanged, this, &FeedsModel::triggerFeedUpdate);
|
||||||
if (data(index(i, 0), UrlRole).toString() == url) {
|
connect(&DataManager::instance(), &DataManager::favoriteEntryCountChanged, this, &FeedsModel::triggerFeedUpdate);
|
||||||
Q_EMIT dataChanged(index(i, 0), index(i, 0));
|
connect(&Fetcher::instance(), &Fetcher::feedDetailsUpdated, this, &FeedsModel::triggerFeedUpdate);
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
connect(&Fetcher::instance(), &Fetcher::feedDetailsUpdated, 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
|
QHash<int, QByteArray> FeedsModel::roleNames() const
|
||||||
@ -53,6 +41,8 @@ QHash<int, QByteArray> FeedsModel::roleNames() const
|
|||||||
{UrlRole, "url"},
|
{UrlRole, "url"},
|
||||||
{TitleRole, "title"},
|
{TitleRole, "title"},
|
||||||
{UnreadCountRole, "unreadCount"},
|
{UnreadCountRole, "unreadCount"},
|
||||||
|
{NewCountRole, "newCount"},
|
||||||
|
{FavoriteCountRole, "favoriteCount"},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,7 +64,21 @@ QVariant FeedsModel::data(const QModelIndex &index, int role) const
|
|||||||
return QVariant::fromValue(DataManager::instance().getFeed(index.row())->name());
|
return QVariant::fromValue(DataManager::instance().getFeed(index.row())->name());
|
||||||
case UnreadCountRole:
|
case UnreadCountRole:
|
||||||
return QVariant::fromValue(DataManager::instance().getFeed(index.row())->unreadEntryCount());
|
return QVariant::fromValue(DataManager::instance().getFeed(index.row())->unreadEntryCount());
|
||||||
|
case NewCountRole:
|
||||||
|
return QVariant::fromValue(DataManager::instance().getFeed(index.row())->newEntryCount());
|
||||||
|
case FavoriteCountRole:
|
||||||
|
return QVariant::fromValue(DataManager::instance().getFeed(index.row())->favoriteEntryCount());
|
||||||
default:
|
default:
|
||||||
return QVariant();
|
return QVariant();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void FeedsModel::triggerFeedUpdate(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -24,6 +24,8 @@ public:
|
|||||||
UrlRole,
|
UrlRole,
|
||||||
TitleRole,
|
TitleRole,
|
||||||
UnreadCountRole,
|
UnreadCountRole,
|
||||||
|
NewCountRole,
|
||||||
|
FavoriteCountRole,
|
||||||
};
|
};
|
||||||
Q_ENUM(Roles)
|
Q_ENUM(Roles)
|
||||||
|
|
||||||
@ -31,4 +33,7 @@ public:
|
|||||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||||
QHash<int, QByteArray> roleNames() const override;
|
QHash<int, QByteArray> roleNames() const override;
|
||||||
int rowCount(const QModelIndex &parent) const override;
|
int rowCount(const QModelIndex &parent) const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void triggerFeedUpdate(const QString &url);
|
||||||
};
|
};
|
||||||
|
@ -6,26 +6,154 @@
|
|||||||
|
|
||||||
#include "models/feedsproxymodel.h"
|
#include "models/feedsproxymodel.h"
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
|
#include <KLocalizedString>
|
||||||
|
|
||||||
FeedsProxyModel::FeedsProxyModel(QObject *parent)
|
FeedsProxyModel::FeedsProxyModel(QObject *parent)
|
||||||
: QSortFilterProxyModel(parent)
|
: QSortFilterProxyModel(parent)
|
||||||
{
|
{
|
||||||
m_feedsModel = new FeedsModel(this);
|
m_feedsModel = new FeedsModel(this);
|
||||||
setSourceModel(m_feedsModel);
|
setSourceModel(m_feedsModel);
|
||||||
setDynamicSortFilter(true);
|
setDynamicSortFilter(true);
|
||||||
sort(0, Qt::AscendingOrder);
|
sort(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FeedsProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
|
bool FeedsProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
|
||||||
{
|
{
|
||||||
QString leftTitle = sourceModel()->data(left, FeedsModel::TitleRole).toString();
|
QString leftTitle = sourceModel()->data(left, FeedsModel::TitleRole).toString();
|
||||||
QString rightTitle = sourceModel()->data(right, 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) {
|
if (m_currentSort == SortType::UnreadDescending || m_currentSort == SortType::UnreadAscending) {
|
||||||
return QString::localeAwareCompare(leftTitle, rightTitle) < 0;
|
int leftUnreadCount = sourceModel()->data(left, FeedsModel::UnreadCountRole).toInt();
|
||||||
} else {
|
int rightUnreadCount = sourceModel()->data(right, FeedsModel::UnreadCountRole).toInt();
|
||||||
return leftUnreadCount > rightUnreadCount;
|
|
||||||
|
if (leftUnreadCount != rightUnreadCount) {
|
||||||
|
if (m_currentSort == SortType::UnreadDescending) {
|
||||||
|
return leftUnreadCount > rightUnreadCount;
|
||||||
|
} else {
|
||||||
|
return leftUnreadCount < rightUnreadCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (m_currentSort == SortType::NewDescending || m_currentSort == SortType::NewAscending) {
|
||||||
|
int leftNewCount = sourceModel()->data(left, FeedsModel::NewCountRole).toInt();
|
||||||
|
int rightNewCount = sourceModel()->data(right, FeedsModel::NewCountRole).toInt();
|
||||||
|
|
||||||
|
if (leftNewCount != rightNewCount) {
|
||||||
|
if (m_currentSort == SortType::NewDescending) {
|
||||||
|
return leftNewCount > rightNewCount;
|
||||||
|
} else {
|
||||||
|
return leftNewCount < rightNewCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (m_currentSort == SortType::FavoriteDescending || m_currentSort == SortType::FavoriteAscending) {
|
||||||
|
int leftFavoriteCount = sourceModel()->data(left, FeedsModel::FavoriteCountRole).toInt();
|
||||||
|
int rightFavoriteCount = sourceModel()->data(right, FeedsModel::FavoriteCountRole).toInt();
|
||||||
|
|
||||||
|
if (leftFavoriteCount != rightFavoriteCount) {
|
||||||
|
if (m_currentSort == SortType::FavoriteDescending) {
|
||||||
|
return leftFavoriteCount > rightFavoriteCount;
|
||||||
|
} else {
|
||||||
|
return leftFavoriteCount < rightFavoriteCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (m_currentSort == SortType::TitleDescending) {
|
||||||
|
return QString::localeAwareCompare(leftTitle, rightTitle) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In case there is a "tie" always use ascending alphabetical ordering
|
||||||
|
return QString::localeAwareCompare(leftTitle, rightTitle) < 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FeedsProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
|
||||||
|
{
|
||||||
|
QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
|
||||||
|
|
||||||
|
bool found = m_searchFilter.isEmpty();
|
||||||
|
if (!m_searchFilter.isEmpty()) {
|
||||||
|
if (sourceModel()->data(index, FeedsModel::Roles::TitleRole).value<QString>().contains(m_searchFilter, Qt::CaseInsensitive)) {
|
||||||
|
found |= true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString FeedsProxyModel::searchFilter() const
|
||||||
|
{
|
||||||
|
return m_searchFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
FeedsProxyModel::SortType FeedsProxyModel::sortType() const
|
||||||
|
{
|
||||||
|
return m_currentSort;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FeedsProxyModel::setSearchFilter(const QString &searchString)
|
||||||
|
{
|
||||||
|
if (searchString != m_searchFilter) {
|
||||||
|
beginResetModel();
|
||||||
|
m_searchFilter = searchString;
|
||||||
|
endResetModel();
|
||||||
|
|
||||||
|
Q_EMIT searchFilterChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FeedsProxyModel::setSortType(SortType type)
|
||||||
|
{
|
||||||
|
if (type != m_currentSort) {
|
||||||
|
m_currentSort = type;
|
||||||
|
|
||||||
|
// HACK: get the list re-sorted with a custom lessThan implementation
|
||||||
|
sort(-1);
|
||||||
|
sort(0);
|
||||||
|
|
||||||
|
Q_EMIT sortTypeChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString FeedsProxyModel::getSortName(SortType type) const
|
||||||
|
{
|
||||||
|
switch (type) {
|
||||||
|
case SortType::UnreadDescending:
|
||||||
|
return i18nc("@label:chooser Sort podcasts by decreasing number of unplayed episodes", "Unplayed count: descending");
|
||||||
|
case SortType::UnreadAscending:
|
||||||
|
return i18nc("@label:chooser Sort podcasts by increasing number of unplayed episodes", "Unplayed count: ascending");
|
||||||
|
case SortType::NewDescending:
|
||||||
|
return i18nc("@label:chooser Sort podcasts by decreasing number of new episodes", "New count: descending");
|
||||||
|
case SortType::NewAscending:
|
||||||
|
return i18nc("@label:chooser Sort podcasts by increasing number of new episodes", "New count: ascending");
|
||||||
|
case SortType::FavoriteDescending:
|
||||||
|
return i18nc("@label:chooser Sort podcasts by decreasing number of favorites", "Favorite count: descending");
|
||||||
|
case SortType::FavoriteAscending:
|
||||||
|
return i18nc("@label:chooser Sort podcasts by increasing number of favorites", "Favorite count: ascending");
|
||||||
|
case SortType::TitleAscending:
|
||||||
|
return i18nc("@label:chooser Sort podcasts titles alphabetically", "Podcast title: A → Z");
|
||||||
|
case SortType::TitleDescending:
|
||||||
|
return i18nc("@label:chooser Sort podcasts titles in reverse alphabetical order", "Podcast title: Z → A");
|
||||||
|
default:
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString FeedsProxyModel::getSortIconName(SortType type) const
|
||||||
|
{
|
||||||
|
switch (type) {
|
||||||
|
case SortType::UnreadDescending:
|
||||||
|
case SortType::NewDescending:
|
||||||
|
case SortType::FavoriteDescending:
|
||||||
|
return QStringLiteral("view-sort-descending");
|
||||||
|
case SortType::UnreadAscending:
|
||||||
|
case SortType::NewAscending:
|
||||||
|
case SortType::FavoriteAscending:
|
||||||
|
return QStringLiteral("view-sort-ascending");
|
||||||
|
case SortType::TitleDescending:
|
||||||
|
return QStringLiteral("view-sort-descending-name");
|
||||||
|
case SortType::TitleAscending:
|
||||||
|
return QStringLiteral("view-sort-ascending-name");
|
||||||
|
default:
|
||||||
|
return QString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QItemSelection>
|
#include <QItemSelection>
|
||||||
|
#include <QModelIndex>
|
||||||
#include <QSortFilterProxyModel>
|
#include <QSortFilterProxyModel>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
#include "models/feedsmodel.h"
|
#include "models/feedsmodel.h"
|
||||||
|
|
||||||
@ -18,12 +20,46 @@ class FeedsProxyModel : public QSortFilterProxyModel
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
enum SortType {
|
||||||
|
UnreadDescending,
|
||||||
|
UnreadAscending,
|
||||||
|
NewDescending,
|
||||||
|
NewAscending,
|
||||||
|
FavoriteDescending,
|
||||||
|
FavoriteAscending,
|
||||||
|
TitleAscending,
|
||||||
|
TitleDescending,
|
||||||
|
};
|
||||||
|
Q_ENUM(SortType)
|
||||||
|
|
||||||
|
Q_PROPERTY(QString searchFilter READ searchFilter WRITE setSearchFilter NOTIFY searchFilterChanged)
|
||||||
|
Q_PROPERTY(SortType sortType READ sortType WRITE setSortType NOTIFY sortTypeChanged)
|
||||||
|
|
||||||
explicit FeedsProxyModel(QObject *parent = nullptr);
|
explicit FeedsProxyModel(QObject *parent = nullptr);
|
||||||
|
|
||||||
bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
|
bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
|
||||||
|
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
|
||||||
|
|
||||||
|
QString searchFilter() const;
|
||||||
|
SortType sortType() const;
|
||||||
|
|
||||||
|
void setSearchFilter(const QString &searchString);
|
||||||
|
void setSortType(SortType type);
|
||||||
|
|
||||||
|
Q_INVOKABLE QString getSortName(SortType type) const;
|
||||||
|
Q_INVOKABLE QString getSortIconName(SortType type) const;
|
||||||
|
|
||||||
Q_INVOKABLE QItemSelection createSelection(int rowa, int rowb);
|
Q_INVOKABLE QItemSelection createSelection(int rowa, int rowb);
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void searchFilterChanged();
|
||||||
|
void sortTypeChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
FeedsModel *m_feedsModel;
|
FeedsModel *m_feedsModel;
|
||||||
|
|
||||||
|
QString m_searchFilter;
|
||||||
|
SortType m_currentSort = SortType::UnreadDescending;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Q_DECLARE_METATYPE(FeedsProxyModel::SortType)
|
||||||
|
@ -18,6 +18,7 @@ import org.kde.kasts 1.0
|
|||||||
Controls.ItemDelegate {
|
Controls.ItemDelegate {
|
||||||
id: feedDelegate
|
id: feedDelegate
|
||||||
|
|
||||||
|
property var countProperty: (kastsMainWindow.feedSorting === FeedsProxyModel.UnreadDescending || kastsMainWindow.feedSorting === FeedsProxyModel.UnreadAscending) ? feed.unreadEntryCount : ((kastsMainWindow.feedSorting === FeedsProxyModel.NewDescending || kastsMainWindow.feedSorting === FeedsProxyModel.NewAscending) ? feed.newEntryCount : ((kastsMainWindow.feedSorting === FeedsProxyModel.FavoriteDescending || kastsMainWindow.feedSorting === FeedsProxyModel.FavoriteAscending) ? feed.favoriteEntryCount : 0))
|
||||||
property int cardSize: 0
|
property int cardSize: 0
|
||||||
property int cardMargin: 0
|
property int cardMargin: 0
|
||||||
property int borderWidth: 1
|
property int borderWidth: 1
|
||||||
@ -170,7 +171,7 @@ Controls.ItemDelegate {
|
|||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: countRectangle
|
id: countRectangle
|
||||||
visible: feed.unreadEntryCount > 0
|
visible: countProperty > 0
|
||||||
anchors.top: img.top
|
anchors.top: img.top
|
||||||
anchors.right: img.right
|
anchors.right: img.right
|
||||||
width: actionsButton.width
|
width: actionsButton.width
|
||||||
@ -181,10 +182,10 @@ Controls.ItemDelegate {
|
|||||||
|
|
||||||
Controls.Label {
|
Controls.Label {
|
||||||
id: countLabel
|
id: countLabel
|
||||||
visible: feed.unreadEntryCount > 0
|
visible: countProperty > 0
|
||||||
anchors.centerIn: countRectangle
|
anchors.centerIn: countRectangle
|
||||||
anchors.margins: Kirigami.Units.smallSpacing
|
anchors.margins: Kirigami.Units.smallSpacing
|
||||||
text: feed.unreadEntryCount
|
text: countProperty
|
||||||
font.bold: true
|
font.bold: true
|
||||||
color: Kirigami.Theme.highlightedTextColor
|
color: Kirigami.Theme.highlightedTextColor
|
||||||
}
|
}
|
||||||
|
@ -57,6 +57,62 @@ Kirigami.ScrollablePage {
|
|||||||
addSheet.open()
|
addSheet.open()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Kirigami.Action {
|
||||||
|
id: sortActionRoot
|
||||||
|
icon.name: "view-sort"
|
||||||
|
text: i18nc("@action:intoolbar Open menu with options to sort subscriptions", "Sort")
|
||||||
|
|
||||||
|
tooltip: i18nc("@info:tooltip", "Select how to sort subscriptions")
|
||||||
|
|
||||||
|
property Controls.ActionGroup sortGroup: Controls.ActionGroup { }
|
||||||
|
|
||||||
|
property Instantiator repeater: Instantiator {
|
||||||
|
model: ListModel {
|
||||||
|
id: sortModel
|
||||||
|
// have to use script because i18n doesn't work within ListElement
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (sortActionRoot.visible) {
|
||||||
|
var sortList = [FeedsProxyModel.UnreadDescending,
|
||||||
|
FeedsProxyModel.UnreadAscending,
|
||||||
|
FeedsProxyModel.NewDescending,
|
||||||
|
FeedsProxyModel.NewAscending,
|
||||||
|
FeedsProxyModel.FavoriteDescending,
|
||||||
|
FeedsProxyModel.FavoriteAscending,
|
||||||
|
FeedsProxyModel.TitleAscending,
|
||||||
|
FeedsProxyModel.TitleDescending]
|
||||||
|
for (var i in sortList) {
|
||||||
|
sortModel.append({"name": feedsModel.getSortName(sortList[i]),
|
||||||
|
"iconName": feedsModel.getSortIconName(sortList[i]),
|
||||||
|
"sortType": sortList[i]});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Kirigami.Action {
|
||||||
|
visible: sortActionRoot.visible
|
||||||
|
icon.name: model.iconName
|
||||||
|
text: model.name
|
||||||
|
checkable: true
|
||||||
|
checked: kastsMainWindow.feedSorting === model.sortType
|
||||||
|
Controls.ActionGroup.group: sortActionRoot.sortGroup
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
kastsMainWindow.feedSorting = model.sortType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onObjectAdded: (index, object) => {
|
||||||
|
sortActionRoot.children.push(object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Kirigami.Action {
|
||||||
|
id: searchActionButton
|
||||||
|
icon.name: "search"
|
||||||
|
text: i18nc("@action:intoolbar", "Search")
|
||||||
|
checkable: true
|
||||||
|
},
|
||||||
Kirigami.Action {
|
Kirigami.Action {
|
||||||
id: importAction
|
id: importAction
|
||||||
text: i18nc("@action:intoolbar", "Import Podcasts…")
|
text: i18nc("@action:intoolbar", "Import Podcasts…")
|
||||||
@ -83,6 +139,19 @@ Kirigami.ScrollablePage {
|
|||||||
// TODO: KF6 replace contextualActions with actions
|
// TODO: KF6 replace contextualActions with actions
|
||||||
contextualActions: pageActions
|
contextualActions: pageActions
|
||||||
|
|
||||||
|
header: Loader {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.left: parent.left
|
||||||
|
|
||||||
|
active: searchActionButton.checked
|
||||||
|
visible: active
|
||||||
|
sourceComponent: SearchBar {
|
||||||
|
proxyModel: feedsModel
|
||||||
|
parentKey: searchActionButton
|
||||||
|
showSearchFilters: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
AddFeedSheet {
|
AddFeedSheet {
|
||||||
id: addSheet
|
id: addSheet
|
||||||
}
|
}
|
||||||
@ -114,9 +183,9 @@ Kirigami.ScrollablePage {
|
|||||||
visible: feedList.count === 0
|
visible: feedList.count === 0
|
||||||
width: Kirigami.Units.gridUnit * 20
|
width: Kirigami.Units.gridUnit * 20
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
type: Kirigami.PlaceholderMessage.Actionable
|
type: feedsModel.searchFilter === "" ? Kirigami.PlaceholderMessage.Actionable : Kirigami.PlaceholderMessage.Informational
|
||||||
text: i18nc("@info Placeholder message for empty podcast list", "No podcasts added yet")
|
text: feedsModel.searchFilter === "" ? i18nc("@info Placeholder message for empty podcast list", "No podcasts added yet") : i18nc("@info Placeholder message for podcast list when no podcast matches the search criteria", "No podcasts found")
|
||||||
explanation: i18nc("@info:tipoftheday", "Get started by adding podcasts:")
|
explanation: feedsModel.searchFilter === "" ? i18nc("@info:tipoftheday", "Get started by adding podcasts:") : null
|
||||||
|
|
||||||
readonly property int buttonSize: Math.max(discoverButton.implicitWidth, addButton.implicitWidth, importButton.implicitWidth, syncButton.implicitWidth)
|
readonly property int buttonSize: Math.max(discoverButton.implicitWidth, addButton.implicitWidth, importButton.implicitWidth, syncButton.implicitWidth)
|
||||||
|
|
||||||
@ -124,6 +193,7 @@ Kirigami.ScrollablePage {
|
|||||||
// to give them more descriptive names
|
// to give them more descriptive names
|
||||||
Controls.Button {
|
Controls.Button {
|
||||||
id: discoverButton
|
id: discoverButton
|
||||||
|
visible: feedsModel.searchFilter === ""
|
||||||
Layout.preferredWidth: placeholderMessage.buttonSize
|
Layout.preferredWidth: placeholderMessage.buttonSize
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
Layout.topMargin: Kirigami.Units.gridUnit
|
Layout.topMargin: Kirigami.Units.gridUnit
|
||||||
@ -136,6 +206,7 @@ Kirigami.ScrollablePage {
|
|||||||
|
|
||||||
Controls.Button {
|
Controls.Button {
|
||||||
id: addButton
|
id: addButton
|
||||||
|
visible: feedsModel.searchFilter === ""
|
||||||
Layout.preferredWidth: placeholderMessage.buttonSize
|
Layout.preferredWidth: placeholderMessage.buttonSize
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
action: addAction
|
action: addAction
|
||||||
@ -143,6 +214,7 @@ Kirigami.ScrollablePage {
|
|||||||
|
|
||||||
Controls.Button {
|
Controls.Button {
|
||||||
id: importButton
|
id: importButton
|
||||||
|
visible: feedsModel.searchFilter === ""
|
||||||
Layout.preferredWidth: placeholderMessage.buttonSize
|
Layout.preferredWidth: placeholderMessage.buttonSize
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
action: importAction
|
action: importAction
|
||||||
@ -150,6 +222,7 @@ Kirigami.ScrollablePage {
|
|||||||
|
|
||||||
Controls.Button {
|
Controls.Button {
|
||||||
id: syncButton
|
id: syncButton
|
||||||
|
visible: feedsModel.searchFilter === ""
|
||||||
Layout.preferredWidth: placeholderMessage.buttonSize
|
Layout.preferredWidth: placeholderMessage.buttonSize
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
action: Kirigami.Action {
|
action: Kirigami.Action {
|
||||||
@ -184,6 +257,7 @@ Kirigami.ScrollablePage {
|
|||||||
|
|
||||||
model: FeedsProxyModel {
|
model: FeedsProxyModel {
|
||||||
id: feedsModel
|
id: feedsModel
|
||||||
|
sortType: kastsMainWindow.feedSorting
|
||||||
}
|
}
|
||||||
|
|
||||||
delegate: FeedListDelegate {
|
delegate: FeedListDelegate {
|
||||||
@ -207,9 +281,9 @@ Kirigami.ScrollablePage {
|
|||||||
Connections {
|
Connections {
|
||||||
target: feedList.model
|
target: feedList.model
|
||||||
function onModelAboutToBeReset() {
|
function onModelAboutToBeReset() {
|
||||||
selectionForContextMenu = [];
|
feedList.selectionForContextMenu = [];
|
||||||
feedList.selectionModel.clear();
|
feedList.selectionModel.clear();
|
||||||
feedList.selectionModel.setCurrentIndex(model.index(0, 0), ItemSelectionModel.Current); // Only set current item; don't select it
|
feedList.selectionModel.setCurrentIndex(feedList.model.index(0, 0), ItemSelectionModel.Current); // Only set current item; don't select it
|
||||||
currentIndex = 0;
|
currentIndex = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ Controls.Control {
|
|||||||
|
|
||||||
required property var proxyModel
|
required property var proxyModel
|
||||||
required property var parentKey
|
required property var parentKey
|
||||||
|
property bool showSearchFilters: true
|
||||||
|
|
||||||
leftPadding: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
|
leftPadding: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
|
||||||
rightPadding: Kirigami.Units.largeSpacing
|
rightPadding: Kirigami.Units.largeSpacing
|
||||||
@ -46,6 +47,8 @@ Controls.Control {
|
|||||||
|
|
||||||
Kirigami.Action {
|
Kirigami.Action {
|
||||||
id: searchSettingsButton
|
id: searchSettingsButton
|
||||||
|
visible: showSearchFilters
|
||||||
|
enabled: visible
|
||||||
icon.name: "settings-configure"
|
icon.name: "settings-configure"
|
||||||
text: i18nc("@action:intoolbar", "Advanced Search Options")
|
text: i18nc("@action:intoolbar", "Advanced Search Options")
|
||||||
|
|
||||||
@ -68,7 +71,6 @@ Controls.Control {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Keys.onEscapePressed: {
|
Keys.onEscapePressed: {
|
||||||
proxyModel.filterType = AbstractEpisodeProxyModel.NoFilter;
|
|
||||||
proxyModel.searchFilter = "";
|
proxyModel.searchFilter = "";
|
||||||
parentKey.checked = false;
|
parentKey.checked = false;
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
@ -88,13 +90,15 @@ Controls.Control {
|
|||||||
|
|
||||||
function reload() {
|
function reload() {
|
||||||
clear();
|
clear();
|
||||||
var searchList = [AbstractEpisodeProxyModel.TitleFlag,
|
if (showSearchFilters) {
|
||||||
AbstractEpisodeProxyModel.ContentFlag,
|
var searchList = [AbstractEpisodeProxyModel.TitleFlag,
|
||||||
AbstractEpisodeProxyModel.FeedNameFlag]
|
AbstractEpisodeProxyModel.ContentFlag,
|
||||||
for (var i in searchList) {
|
AbstractEpisodeProxyModel.FeedNameFlag]
|
||||||
searchSettingsModel.append({"name": proxyModel.getSearchFlagName(searchList[i]),
|
for (var i in searchList) {
|
||||||
"searchFlag": searchList[i],
|
searchSettingsModel.append({"name": proxyModel.getSearchFlagName(searchList[i]),
|
||||||
"checked": proxyModel.searchFlags & searchList[i]});
|
"searchFlag": searchList[i],
|
||||||
|
"checked": proxyModel.searchFlags & searchList[i]});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,6 +43,7 @@ Kirigami.ApplicationWindow {
|
|||||||
}
|
}
|
||||||
property var lastFeed: ""
|
property var lastFeed: ""
|
||||||
property string currentPage: ""
|
property string currentPage: ""
|
||||||
|
property int feedSorting: FeedsProxyModel.UnreadDescending
|
||||||
|
|
||||||
property bool isWidescreen: kastsMainWindow.width > kastsMainWindow.height
|
property bool isWidescreen: kastsMainWindow.width > kastsMainWindow.height
|
||||||
|
|
||||||
@ -85,6 +86,7 @@ Kirigami.ApplicationWindow {
|
|||||||
property var desktopHeight
|
property var desktopHeight
|
||||||
property int headerSize: Kirigami.Units.gridUnit * 5
|
property int headerSize: Kirigami.Units.gridUnit * 5
|
||||||
property alias lastOpenedPage: kastsMainWindow.currentPage
|
property alias lastOpenedPage: kastsMainWindow.currentPage
|
||||||
|
property alias feedSorting: kastsMainWindow.feedSorting
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveWindowLayout() {
|
function saveWindowLayout() {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user