diff --git a/src/database.cpp b/src/database.cpp index 49a3ad5f..de12c73e 100644 --- a/src/database.cpp +++ b/src/database.cpp @@ -50,7 +50,7 @@ bool Database::migrateTo1() TRUE_OR_RETURN(execute(QStringLiteral("CREATE TABLE IF NOT EXISTS Feeds (name TEXT, url TEXT, image TEXT, link TEXT, description TEXT, deleteAfterCount INTEGER, deleteAfterType INTEGER, subscribed INTEGER, lastUpdated INTEGER, new BOOL, notify BOOL);"))); TRUE_OR_RETURN(execute(QStringLiteral("CREATE TABLE IF NOT EXISTS Entries (feed TEXT, id TEXT UNIQUE, title TEXT, content TEXT, created INTEGER, updated INTEGER, link TEXT, read bool, new bool, hasEnclosure BOOL, image TEXT);"))); TRUE_OR_RETURN(execute(QStringLiteral("CREATE TABLE IF NOT EXISTS Authors (feed TEXT, id TEXT, name TEXT, uri TEXT, email TEXT);"))); - TRUE_OR_RETURN(execute(QStringLiteral("CREATE TABLE IF NOT EXISTS Enclosures (feed TEXT, id TEXT, duration INTEGER, size INTEGER, title TEXT, type TEXT, url TEXT, playposition INTEGER);"))); //, filename TEXT);"))); + TRUE_OR_RETURN(execute(QStringLiteral("CREATE TABLE IF NOT EXISTS Enclosures (feed TEXT, id TEXT, duration INTEGER, size INTEGER, title TEXT, type TEXT, url TEXT, playposition INTEGER, downloaded BOOL);"))); //, filename TEXT);"))); TRUE_OR_RETURN(execute(QStringLiteral("CREATE TABLE IF NOT EXISTS Queue (listnr INTEGER, feed TEXT, id TEXT, playing BOOL);"))); TRUE_OR_RETURN(execute(QStringLiteral("PRAGMA user_version = 1;"))); return true; diff --git a/src/datamanager.cpp b/src/datamanager.cpp index f22b77d0..27ed4df5 100644 --- a/src/datamanager.cpp +++ b/src/datamanager.cpp @@ -141,15 +141,19 @@ Entry* DataManager::getEntry(QString id) const Entry* DataManager::getEntry(const EpisodeModel::Type type, const int entry_index) const { QSqlQuery entryQuery; - if (type == EpisodeModel::All || type == EpisodeModel::New || type == EpisodeModel::Unread) { + if (type == EpisodeModel::All || type == EpisodeModel::New || type == EpisodeModel::Unread || type == EpisodeModel::Downloaded) { + if (type == EpisodeModel::New) { - entryQuery.prepare(QStringLiteral("SELECT * FROM Entries WHERE new=:new ORDER BY updated DESC LIMIT 1 OFFSET :index;")); + entryQuery.prepare(QStringLiteral("SELECT id FROM Entries WHERE new=:new ORDER BY updated DESC LIMIT 1 OFFSET :index;")); entryQuery.bindValue(QStringLiteral(":new"), true); } else if (type == EpisodeModel::Unread) { - entryQuery.prepare(QStringLiteral("SELECT * FROM Entries WHERE read=:read ORDER BY updated DESC LIMIT 1 OFFSET :index;")); + entryQuery.prepare(QStringLiteral("SELECT id FROM Entries WHERE read=:read ORDER BY updated DESC LIMIT 1 OFFSET :index;")); entryQuery.bindValue(QStringLiteral(":read"), false); - } else { // i.e. EpisodeModel::All - entryQuery.prepare(QStringLiteral("SELECT * FROM Entries ORDER BY updated DESC LIMIT 1 OFFSET :index;")); + } else if (type == EpisodeModel::All) { + entryQuery.prepare(QStringLiteral("SELECT id FROM Entries ORDER BY updated DESC LIMIT 1 OFFSET :index;")); + } else { // i.e. EpisodeModel::Downloaded + entryQuery.prepare(QStringLiteral("SELECT * FROM Enclosures INNER JOIN Entries ON Enclosures.id = Entries.id WHERE downloaded=:downloaded ORDER BY updated DESC LIMIT 1 OFFSET :index;")); + entryQuery.bindValue(QStringLiteral(":downloaded"), true); } entryQuery.bindValue(QStringLiteral(":index"), entry_index); Database::instance().execute(entryQuery); @@ -159,6 +163,7 @@ Entry* DataManager::getEntry(const EpisodeModel::Type type, const int entry_inde } QString id = entryQuery.value(QStringLiteral("id")).toString(); return getEntry(id); + } return nullptr; } @@ -181,15 +186,18 @@ int DataManager::entryCount(const Feed* feed) const int DataManager::entryCount(const EpisodeModel::Type type) const { QSqlQuery query; - if (type == EpisodeModel::All || type == EpisodeModel::New || type == EpisodeModel::Unread) { + if (type == EpisodeModel::All || type == EpisodeModel::New || type == EpisodeModel::Unread || type == EpisodeModel::Downloaded) { if (type == EpisodeModel::New) { query.prepare(QStringLiteral("SELECT COUNT (id) FROM Entries WHERE new=:new;")); query.bindValue(QStringLiteral(":new"), true); } else if (type == EpisodeModel::Unread) { query.prepare(QStringLiteral("SELECT COUNT (id) FROM Entries WHERE read=:read;")); query.bindValue(QStringLiteral(":read"), false); - } else { // i.e. EpisodeModel::All + } else if (type == EpisodeModel::All) { query.prepare(QStringLiteral("SELECT COUNT (id) FROM Entries;")); + } else { // i.e. EpisodeModel::Downloaded + query.prepare(QStringLiteral("SELECT COUNT (id) FROM Enclosures WHERE downloaded=:downloaded;")); + query.bindValue(QStringLiteral(":downloaded"), true); } Database::instance().execute(query); if (!query.next()) diff --git a/src/datamanager.h b/src/datamanager.h index 96c40072..43db202b 100644 --- a/src/datamanager.h +++ b/src/datamanager.h @@ -71,6 +71,7 @@ Q_SIGNALS: void unreadEntryCountChanged(const QString &url); void newEntryCountChanged(const QString &url); + void downloadCountChanged(const QString &url); private: DataManager(); diff --git a/src/enclosure.cpp b/src/enclosure.cpp index 45348106..9a0b4595 100644 --- a/src/enclosure.cpp +++ b/src/enclosure.cpp @@ -12,6 +12,7 @@ #include #include "database.h" +#include "datamanager.h" #include "enclosuredownloadjob.h" #include "entry.h" #include "fetcher.h" @@ -35,18 +36,46 @@ Enclosure::Enclosure(Entry *entry) m_type = query.value(QStringLiteral("type")).toString(); m_url = query.value(QStringLiteral("url")).toString(); m_playposition = query.value(QStringLiteral("playposition")).toLongLong(); + m_status = query.value(QStringLiteral("downloaded")).toBool() ? Downloaded : Downloadable; m_playposition_dbsave = m_playposition; + // In principle the database contains this status, we check anyway in case + // something changed on disk QFile file(path()); if (file.exists()) { if(file.size() == m_size) { - m_status = Downloaded; + if (m_status == Downloadable) { + // file is on disk, but was not expected, write to database + // this should never happen + m_status = Downloaded; + query.prepare(QStringLiteral("UPDATE Enclosures SET downloaded=:downloaded WHERE id=:id;")); + query.bindValue(QStringLiteral(":id"), entry->id()); + query.bindValue(QStringLiteral(":downloaded"), true); + Database::instance().execute(query); + } } else { - file.remove(); - m_status = Downloadable; + if (m_status == Downloaded) { + // file was downloaded, but there is a size mismatch + // delete file and update status in database + file.remove(); + m_status = Downloadable; + query.prepare(QStringLiteral("UPDATE Enclosures SET downloaded=:downloaded WHERE id=:id;")); + query.bindValue(QStringLiteral(":id"), entry->id()); + query.bindValue(QStringLiteral(":downloaded"), false); + Database::instance().execute(query); + } } } else { - m_status = Downloadable; + if (m_status == Downloaded) { + // file was supposed to be on disk, but isn't there + // update status and write to the database + file.remove(); + m_status = Downloadable; + query.prepare(QStringLiteral("UPDATE Enclosures SET downloaded=:downloaded WHERE id=:id;")); + query.bindValue(QStringLiteral(":id"), entry->id()); + query.bindValue(QStringLiteral(":downloaded"), false); + Database::instance().execute(query); + } } } @@ -63,9 +92,7 @@ void Enclosure::download() connect(downloadJob, &KJob::result, this, [this, downloadJob]() { if(downloadJob->error() == 0) { - m_status = Downloaded; processDownloadedFile(); - } else { m_status = Downloadable; if(downloadJob->error() != QNetworkReply::OperationCanceledError) { @@ -81,6 +108,7 @@ void Enclosure::download() downloadJob->doKill(); m_status = Downloadable; Q_EMIT statusChanged(); + Q_EMIT DataManager::instance().downloadCountChanged(m_entry->feed()->url()); disconnect(this, &Enclosure::cancelDownload, this, nullptr); }); @@ -95,7 +123,12 @@ void Enclosure::download() void Enclosure::processDownloadedFile() { // This will be run if the enclosure has been downloaded successfully - + m_status = Downloaded; + QSqlQuery query; + query.prepare(QStringLiteral("UPDATE Enclosures SET downloaded=:downloaded WHERE id=:id;")); + query.bindValue(QStringLiteral(":id"), m_entry->id()); + query.bindValue(QStringLiteral(":downloaded"), true); + Database::instance().execute(query); // Unset "new" status of item if (m_entry->getNew()) m_entry->setNew(false); @@ -113,6 +146,8 @@ void Enclosure::processDownloadedFile() { query.bindValue(QStringLiteral(":size"), m_size); Database::instance().execute(query); } + Q_EMIT DataManager::instance().downloadCountChanged(m_entry->feed()->url()); + } void Enclosure::deleteFile() @@ -123,8 +158,13 @@ void Enclosure::deleteFile() QFile(path()).remove(); // If file disappeared unexpectedly, then still change status to downloadable m_status = Downloadable; + QSqlQuery query; + query.prepare(QStringLiteral("UPDATE Enclosures SET downloaded=:downloaded WHERE id=:id;")); + query.bindValue(QStringLiteral(":id"), m_entry->id()); + query.bindValue(QStringLiteral(":downloaded"), false); + Database::instance().execute(query); Q_EMIT statusChanged(); - + Q_EMIT DataManager::instance().downloadCountChanged(m_entry->feed()->url()); } QString Enclosure::path() const @@ -136,7 +176,6 @@ Enclosure::Status Enclosure::status() const { return m_status; } - qint64 Enclosure::playPosition() const{ return m_playposition; } diff --git a/src/episodemodel.cpp b/src/episodemodel.cpp index d629b750..f2590321 100644 --- a/src/episodemodel.cpp +++ b/src/episodemodel.cpp @@ -57,5 +57,20 @@ void EpisodeModel::setType(EpisodeModel::Type type) beginResetModel(); endResetModel(); }); + } else if (m_type == EpisodeModel::Unread) { + connect(&DataManager::instance(), &DataManager::unreadEntryCountChanged, 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(); + }); + } + else if (m_type == EpisodeModel::Downloaded) { // TODO: this needs to be removed !!!!!! + connect(&DataManager::instance(), &DataManager::downloadCountChanged, 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/fetcher.cpp b/src/fetcher.cpp index 68da2263..407d1a10 100644 --- a/src/fetcher.cpp +++ b/src/fetcher.cpp @@ -255,9 +255,9 @@ void Fetcher::processEnclosure(Syndication::EnclosurePtr enclosure, Syndication: query.next(); if (query.value(0).toInt() != 0) - query.prepare(QStringLiteral("UPDATE Enclosures SET feed=:feed, id=:id, duration=:duration, size=:size, title=:title, type=:type, url=:url, playposition=:playposition;")); + query.prepare(QStringLiteral("UPDATE Enclosures SET feed=:feed, id=:id, duration=:duration, size=:size, title=:title, type=:type, url=:url;")); else - query.prepare(QStringLiteral("INSERT INTO Enclosures VALUES (:feed, :id, :duration, :size, :title, :type, :url, :playposition);")); + query.prepare(QStringLiteral("INSERT INTO Enclosures VALUES (:feed, :id, :duration, :size, :title, :type, :url, :playposition, :downloaded);")); query.bindValue(QStringLiteral(":feed"), feedUrl); query.bindValue(QStringLiteral(":id"), entry->id()); @@ -267,6 +267,7 @@ void Fetcher::processEnclosure(Syndication::EnclosurePtr enclosure, Syndication: query.bindValue(QStringLiteral(":type"), enclosure->type()); query.bindValue(QStringLiteral(":url"), enclosure->url()); query.bindValue(QStringLiteral(":playposition"), 0); + query.bindValue(QStringLiteral(":downloaded"), false); Database::instance().execute(query); } diff --git a/src/qml/DownloadSwipePage.qml b/src/qml/DownloadSwipePage.qml new file mode 100644 index 00000000..4a233349 --- /dev/null +++ b/src/qml/DownloadSwipePage.qml @@ -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 + */ + +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.15 as Kirigami + +import org.kde.alligator 1.0 + +Kirigami.Page { + id: page + + title: i18n("Downloads") + padding: 0 + + header: Loader { + id: headerLoader + active: !Kirigami.Settings.isMobile + sourceComponent: tabBarComponent + property var swipeViewItem: swipeView + } + + footer: Loader { + id: footerLoader + active: Kirigami.Settings.isMobile + sourceComponent: tabBarComponent + property var swipeViewItem: swipeView + } + + Component { + id: tabBarComponent + Controls.TabBar { + id: tabBar + position: Controls.TabBar.Footer + currentIndex: swipeViewItem.currentIndex + + Controls.TabButton { + width: parent.parent.width/parent.count + height: Kirigami.Units.gridUnit * 2 + text: i18n("Downloaded") + } + } + } + + Controls.SwipeView { + id: swipeView + anchors.fill: parent + currentIndex: Kirigami.Settings.isMobile ? footerLoader.item.currentIndex : headerLoader.item.currentIndex + + EpisodeListPage { + title: i18n("Downloaded") + episodeType: EpisodeModel.Downloaded + } + } +} diff --git a/src/qml/main.qml b/src/qml/main.qml index 845ec767..c02557e0 100644 --- a/src/qml/main.qml +++ b/src/qml/main.qml @@ -28,6 +28,7 @@ Kirigami.ApplicationWindow { pageStack.initialPage: mainPagePool.loadPage(SettingsManager.lastOpenedPage === "FeedListPage" ? "qrc:/FeedListPage.qml" : SettingsManager.lastOpenedPage === "QueuePage" ? "qrc:/QueuePage.qml" : SettingsManager.lastOpenedPage === "EpisodeSwipePage" ? "qrc:/EpisodeSwipePage.qml" + : SettingsManager.lastOpenedPage === "DownloadSwipePage" ? "qrc:/DownloadSwipePage.qml" : "qrc:/FeedListPage.qml") globalDrawer: Kirigami.GlobalDrawer { @@ -63,6 +64,15 @@ Kirigami.ApplicationWindow { SettingsManager.lastOpenedPage = "FeedListPage" // for persistency } }, + Kirigami.PagePoolAction { + text: i18n("Downloads") + iconName: "download" + pagePool: mainPagePool + page: "qrc:/DownloadSwipePage.qml" + onTriggered: { + SettingsManager.lastOpenedPage = "DownloadSwipePage" // for persistency + } + }, Kirigami.PagePoolAction { text: i18n("Settings") iconName: "settings-configure" diff --git a/src/resources.qrc b/src/resources.qrc index 53e343a5..21500ee5 100755 --- a/src/resources.qrc +++ b/src/resources.qrc @@ -15,6 +15,7 @@ qml/QueuePage.qml qml/EpisodeListPage.qml qml/EpisodeSwipePage.qml + qml/DownloadSwipePage.qml qml/GenericListHeader.qml qml/GenericEntryDelegate.qml ../logo.png