Implement Downloads page

This change includes an update to the database, adding a downloaded
column in Enclosures.
This commit is contained in:
Bart De Vries 2021-04-18 21:56:33 +02:00
parent 98bc7ffa61
commit e9d20ec569
9 changed files with 155 additions and 19 deletions

View File

@ -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;

View File

@ -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())

View File

@ -71,6 +71,7 @@ Q_SIGNALS:
void unreadEntryCountChanged(const QString &url);
void newEntryCountChanged(const QString &url);
void downloadCountChanged(const QString &url);
private:
DataManager();

View File

@ -12,6 +12,7 @@
#include <QSqlQuery>
#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;
}

View File

@ -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();
});
}
}

View File

@ -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);
}

View File

@ -0,0 +1,61 @@
/**
* 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
*/
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
}
}
}

View File

@ -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"

View File

@ -15,6 +15,7 @@
<file alias="QueuePage.qml">qml/QueuePage.qml</file>
<file alias="EpisodeListPage.qml">qml/EpisodeListPage.qml</file>
<file alias="EpisodeSwipePage.qml">qml/EpisodeSwipePage.qml</file>
<file alias="DownloadSwipePage.qml">qml/DownloadSwipePage.qml</file>
<file alias="GenericListHeader.qml">qml/GenericListHeader.qml</file>
<file alias="GenericEntryDelegate.qml">qml/GenericEntryDelegate.qml</file>
<file alias="logo.png">../logo.png</file>