diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 36860133..0d6b0ecb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -10,6 +10,7 @@ add_executable(alligator enclosure.cpp enclosuredownloadjob.cpp queuemodel.cpp + episodemodel.cpp datamanager.cpp audiomanager.cpp powermanagementinterface.cpp diff --git a/src/datamanager.cpp b/src/datamanager.cpp index e24d741e..d67d7714 100644 --- a/src/datamanager.cpp +++ b/src/datamanager.cpp @@ -138,6 +138,28 @@ Entry* DataManager::getEntry(QString id) const return m_entries[id]; } +Entry* DataManager::getEntry(const EpisodeModel::Type type, const int entry_index) const +{ + QSqlQuery entryQuery; + if (type == EpisodeModel::All || type == EpisodeModel::New) { + if (type == EpisodeModel::New) { + entryQuery.prepare(QStringLiteral("SELECT * FROM Entries WHERE new=:new ORDER BY updated DESC LIMIT 1 OFFSET :index;")); + entryQuery.bindValue(QStringLiteral(":new"), true); + } else { // i.e. EpisodeModel::All + entryQuery.prepare(QStringLiteral("SELECT * FROM Entries ORDER BY updated DESC LIMIT 1 OFFSET :index;")); + } + entryQuery.bindValue(QStringLiteral(":index"), entry_index); + Database::instance().execute(entryQuery); + if (!entryQuery.next()) { + qWarning() << "No element with index" << entry_index << "found"; + return nullptr; + } + QString id = entryQuery.value(QStringLiteral("id")).toString(); + return getEntry(id); + } + return nullptr; +} + int DataManager::feedCount() const { return m_feedmap.count(); @@ -153,6 +175,24 @@ int DataManager::entryCount(const Feed* feed) const return m_entrymap[feed->url()].count(); } +int DataManager::entryCount(const EpisodeModel::Type type) const +{ + QSqlQuery query; + if (type == EpisodeModel::All || type == EpisodeModel::New) { + if (type == EpisodeModel::New) { + query.prepare(QStringLiteral("SELECT COUNT (id) FROM Entries WHERE new=:new;")); + query.bindValue(QStringLiteral(":new"), true); + } else { // i.e. EpisodeModel::All + query.prepare(QStringLiteral("SELECT COUNT (id) FROM Entries;")); + } + Database::instance().execute(query); + if (!query.next()) + return -1; + return query.value(0).toInt(); + } + return -1; +} + int DataManager::unreadEntryCount(const Feed* feed) const { QSqlQuery query; diff --git a/src/datamanager.h b/src/datamanager.h index 0ddc5b2f..96c40072 100644 --- a/src/datamanager.h +++ b/src/datamanager.h @@ -8,6 +8,7 @@ #include "feed.h" #include "entry.h" +#include "episodemodel.h" class DataManager : public QObject { @@ -24,10 +25,12 @@ public: Feed* getFeed(QString const feedurl) const; Entry* getEntry(int const feed_index, int const entry_index) const; Entry* getEntry(const Feed* feed, int const entry_index) const; + Entry* getEntry(const EpisodeModel::Type type, const int entry_index) const; Q_INVOKABLE Entry* getEntry(const QString id) const; int feedCount() const; int entryCount(const int feed_index) const; int entryCount(const Feed* feed) const; + int entryCount(const EpisodeModel::Type type) const; int unreadEntryCount(const Feed* feed) const; int newEntryCount(const Feed* feed) const; Q_INVOKABLE void addFeed(const QString &url); @@ -66,6 +69,9 @@ Q_SIGNALS: void queueEntryRemoved(const int &index, const QString &id); void queueEntryMoved(const int &from, const int &to); + void unreadEntryCountChanged(const QString &url); + void newEntryCountChanged(const QString &url); + private: DataManager(); void loadFeed(QString feedurl) const; diff --git a/src/entry.cpp b/src/entry.cpp index 74192f07..7af57a91 100644 --- a/src/entry.cpp +++ b/src/entry.cpp @@ -133,6 +133,8 @@ void Entry::setRead(const bool read) query.bindValue(QStringLiteral(":read"), m_read); Database::instance().execute(query); Q_EMIT m_feed->unreadEntryCountChanged(); + Q_EMIT DataManager::instance().unreadEntryCountChanged(m_feed->url()); + //TODO: can one of the two slots be removed?? } void Entry::setNew(const bool state) @@ -146,6 +148,7 @@ void Entry::setNew(const bool state) query.bindValue(QStringLiteral(":new"), m_new); Database::instance().execute(query); // Q_EMIT m_feed->newEntryCountChanged(); // TODO: signal and slots to be implemented + Q_EMIT DataManager::instance().newEntryCountChanged(m_feed->url()); } QString Entry::adjustedContent(int width, int fontSize) diff --git a/src/episodemodel.cpp b/src/episodemodel.cpp new file mode 100644 index 00000000..d629b750 --- /dev/null +++ b/src/episodemodel.cpp @@ -0,0 +1,61 @@ +/** + * SPDX-FileCopyrightText: 2021 Bart De Vries + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "episodemodel.h" +#include "datamanager.h" + + +EpisodeModel::EpisodeModel() + : QAbstractListModel(nullptr) +{ + // 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 + // maybe even items have been removed. + connect(&DataManager::instance(), &DataManager::feedEntriesUpdated, this, [this](const QString &url) { + // we have to reset the entire model in case entries are removed or added + // because we have no way of knowing where those entries will be added/removed + beginResetModel(); + endResetModel(); + }); +} + +QVariant EpisodeModel::data(const QModelIndex &index, int role) const +{ + if (role != 0) + return QVariant(); + return QVariant::fromValue(DataManager::instance().getEntry(m_type, index.row())); +} + +QHash EpisodeModel::roleNames() const +{ + QHash roleNames; + roleNames[0] = "entry"; + return roleNames; +} + +int EpisodeModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return DataManager::instance().entryCount(m_type); +} + +EpisodeModel::Type EpisodeModel::type() const +{ + return m_type; +} + +void EpisodeModel::setType(EpisodeModel::Type type) +{ + m_type = type; + if (m_type == EpisodeModel::New) { + connect(&DataManager::instance(), &DataManager::newEntryCountChanged, this, [this](const QString &url) { + // we have to reset the entire model in case entries are removed or added + // because we have no way of knowing where those entries will be added/removed + beginResetModel(); + endResetModel(); + }); + } +} diff --git a/src/episodemodel.h b/src/episodemodel.h new file mode 100644 index 00000000..f2149cab --- /dev/null +++ b/src/episodemodel.h @@ -0,0 +1,41 @@ +/** + * SPDX-FileCopyrightText: 2021 Bart De Vries + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include +#include +#include +#include + +class EpisodeModel : public QAbstractListModel +{ + Q_OBJECT + +public: + enum Type { + All, + New, + Downloading, + Downloaded, + }; + Q_ENUM(Type) + + Q_PROPERTY(Type type READ type WRITE setType) + + explicit EpisodeModel(); + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash roleNames() const override; + int rowCount(const QModelIndex &parent) const override; + + Type type() const; + +public Q_SLOTS: + void setType(Type type); + +private: + Type m_type = Type::All; +}; diff --git a/src/main.cpp b/src/main.cpp index ea788424..bb8b068e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -29,6 +29,7 @@ #include "feedsmodel.h" #include "fetcher.h" #include "queuemodel.h" +#include "episodemodel.h" #include "datamanager.h" #include "audiomanager.h" #include "mpris2/mpris2.h" @@ -51,6 +52,7 @@ int main(int argc, char *argv[]) qmlRegisterType("org.kde.alligator", 1, 0, "FeedsModel"); qmlRegisterType("org.kde.alligator", 1, 0, "QueueModel"); + qmlRegisterType("org.kde.alligator", 1, 0, "EpisodeModel"); qmlRegisterUncreatableType("org.kde.alligator", 1, 0, "EntriesModel", QStringLiteral("Get from Feed")); qmlRegisterUncreatableType("org.kde.alligator", 1, 0, "Enclosure", QStringLiteral("Only for enums")); qmlRegisterSingletonType("org.kde.alligator", 1, 0, "Fetcher", [](QQmlEngine *engine, QJSEngine *) -> QObject * { diff --git a/src/qml/EpisodeListPage.qml b/src/qml/EpisodeListPage.qml new file mode 100644 index 00000000..736d87ab --- /dev/null +++ b/src/qml/EpisodeListPage.qml @@ -0,0 +1,63 @@ +/** + * SPDX-FileCopyrightText: 2020 Tobias Fella + * SPDX-FileCopyrightText: 2021 Bart De Vries + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +import QtQuick 2.14 +import QtQuick.Controls 2.14 as Controls +import QtQuick.Layouts 1.14 +import QtGraphicalEffects 1.15 +import QtMultimedia 5.15 +import org.kde.kirigami 2.12 as Kirigami + +import org.kde.alligator 1.0 + +Kirigami.ScrollablePage { + id: page + + title: i18n("Episode List") + + supportsRefreshing: true + onRefreshingChanged: { + if(refreshing) { + Fetcher.fetchAll() + refreshing = false + } + } + + actions.main: Kirigami.Action { + text: i18n("Refresh all feeds") + iconName: "view-refresh" + onTriggered: refreshing = true + visible: !Kirigami.Settings.isMobile + } + + Kirigami.PlaceholderMessage { + visible: episodeList.count === 0 + + width: Kirigami.Units.gridUnit * 20 + anchors.centerIn: parent + + text: i18n("No Entries available") + } + + Component { + id: entryListDelegate + GenericEntryDelegate { + listView: episodeList + } + } + + ListView { + id: episodeList + visible: count !== 0 + model: EpisodeModel { type: EpisodeModel.All } + + delegate: Kirigami.DelegateRecycler { + width: episodeList.width + sourceComponent: entryListDelegate + } + } +} diff --git a/src/qml/main.qml b/src/qml/main.qml index aafe876a..bef7ac32 100644 --- a/src/qml/main.qml +++ b/src/qml/main.qml @@ -27,6 +27,7 @@ Kirigami.ApplicationWindow { pageStack.initialPage: mainPagePool.loadPage(SettingsManager.lastOpenedPage === "FeedListPage" ? "qrc:/FeedListPage.qml" : SettingsManager.lastOpenedPage === "QueuePage" ? "qrc:/QueuePage.qml" + : SettingsManager.lastOpenedPage === "EpisodeListPage" ? "qrc:/EpisodeListPage.qml" : "qrc:/FeedListPage.qml") globalDrawer: Kirigami.GlobalDrawer { @@ -45,9 +46,18 @@ Kirigami.ApplicationWindow { } }, Kirigami.PagePoolAction { - text: i18n("Subscriptions") + text: i18n("Episodes") iconName: "rss" pagePool: mainPagePool + page: "qrc:/EpisodeListPage.qml" + onTriggered: { + SettingsManager.lastOpenedPage = "EpisodeListPage" // for persistency + } + }, + Kirigami.PagePoolAction { + text: i18n("Subscriptions") + iconName: "document-open-folder" + pagePool: mainPagePool page: "qrc:/FeedListPage.qml" onTriggered: { SettingsManager.lastOpenedPage = "FeedListPage" // for persistency diff --git a/src/resources.qrc b/src/resources.qrc index 21689652..82b9f407 100755 --- a/src/resources.qrc +++ b/src/resources.qrc @@ -13,6 +13,7 @@ qml/PlayerControls.qml qml/FooterBar.qml qml/QueuePage.qml + qml/EpisodeListPage.qml qml/GenericListHeader.qml qml/GenericEntryDelegate.qml ../logo.png