Rework Download list page

This page now has section headers, and show Downloading, Partially
Downloaded and Downloaded enclosures with relevant actions for each.
This commit is contained in:
Bart De Vries 2021-06-08 23:09:30 +02:00
parent f197e6ab02
commit d7debaaf30
14 changed files with 192 additions and 174 deletions

View File

@ -14,7 +14,7 @@ set(SRCS_base
enclosuredownloadjob.cpp
queuemodel.cpp
episodemodel.cpp
downloadprogressmodel.cpp
downloadmodel.cpp
datamanager.cpp
audiomanager.cpp
powermanagementinterface.cpp
@ -39,9 +39,9 @@ ecm_qt_declare_logging_category(SRCS_base
)
ecm_qt_declare_logging_category(SRCS_base
HEADER "downloadprogressmodellogging.h"
IDENTIFIER "kastsDownloadProgressModel"
CATEGORY_NAME "org.kde.kasts.downloadprogressmodel"
HEADER "downloadmodellogging.h"
IDENTIFIER "kastsDownloadModel"
CATEGORY_NAME "org.kde.kasts.downloadmodel"
DEFAULT_SEVERITY Info
)

View File

@ -148,7 +148,8 @@ Entry *DataManager::getEntry(const 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 || type == EpisodeModel::Downloaded) {
if (type == EpisodeModel::All || type == EpisodeModel::New || type == EpisodeModel::Unread || type == EpisodeModel::Downloading
|| type == EpisodeModel::PartiallyDownloaded || type == EpisodeModel::Downloaded) {
if (type == EpisodeModel::New) {
entryQuery.prepare(QStringLiteral("SELECT id FROM Entries WHERE new=:new ORDER BY updated DESC LIMIT 1 OFFSET :index;"));
entryQuery.bindValue(QStringLiteral(":new"), true);
@ -157,16 +158,24 @@ Entry *DataManager::getEntry(const EpisodeModel::Type type, const int entry_inde
entryQuery.bindValue(QStringLiteral(":read"), false);
} else if (type == EpisodeModel::All) {
entryQuery.prepare(QStringLiteral("SELECT id FROM Entries ORDER BY updated DESC LIMIT 1 OFFSET :index;"));
} else if (type == EpisodeModel::Downloading) {
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"), Enclosure::statusToDb(Enclosure::Downloading));
} else if (type == EpisodeModel::PartiallyDownloaded) {
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"), Enclosure::statusToDb(Enclosure::PartiallyDownloaded));
} else if (type == 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"), Enclosure::statusToDb(Enclosure::Downloaded));
} else { // i.e. EpisodeModel::PartiallyDownloaded
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"), Enclosure::statusToDb(Enclosure::PartiallyDownloaded));
} else {
// this should not happen
qWarning() << "Cannot find entry type" << type << "in getEntry for entry index" << entry_index;
}
entryQuery.bindValue(QStringLiteral(":index"), entry_index);
Database::instance().execute(entryQuery);
@ -177,6 +186,7 @@ Entry *DataManager::getEntry(const EpisodeModel::Type type, const int entry_inde
QString id = entryQuery.value(QStringLiteral("id")).toString();
return getEntry(id);
}
qWarning() << "Cannot find entry type" << type << "in getEntry for entry index" << entry_index;
return nullptr;
}
@ -198,7 +208,8 @@ 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 || type == EpisodeModel::Downloaded) {
if (type == EpisodeModel::All || type == EpisodeModel::New || type == EpisodeModel::Unread || type == EpisodeModel::Downloading
|| type == EpisodeModel::PartiallyDownloaded || type == EpisodeModel::Downloaded) {
if (type == EpisodeModel::New) {
query.prepare(QStringLiteral("SELECT COUNT (id) FROM Entries WHERE new=:new;"));
query.bindValue(QStringLiteral(":new"), true);
@ -207,18 +218,25 @@ int DataManager::entryCount(const EpisodeModel::Type type) const
query.bindValue(QStringLiteral(":read"), false);
} else if (type == EpisodeModel::All) {
query.prepare(QStringLiteral("SELECT COUNT (id) FROM Entries;"));
} else if (type == EpisodeModel::Downloading) {
query.prepare(QStringLiteral("SELECT COUNT (id) FROM Enclosures WHERE downloaded=:downloaded;"));
query.bindValue(QStringLiteral(":downloaded"), Enclosure::statusToDb(Enclosure::Downloading));
} else if (type == EpisodeModel::PartiallyDownloaded) {
query.prepare(QStringLiteral("SELECT COUNT (id) FROM Enclosures WHERE downloaded=:downloaded;"));
query.bindValue(QStringLiteral(":downloaded"), Enclosure::statusToDb(Enclosure::PartiallyDownloaded));
} else if (type == EpisodeModel::Downloaded) {
query.prepare(QStringLiteral("SELECT COUNT (id) FROM Enclosures WHERE downloaded=:downloaded;"));
query.bindValue(QStringLiteral(":downloaded"), Enclosure::statusToDb(Enclosure::Downloaded));
} else { // i.e. EpisodeModel::PartiallyDownloaded
query.prepare(QStringLiteral("SELECT COUNT (id) FROM Enclosures WHERE downloaded=:downloaded;"));
query.bindValue(QStringLiteral(":downloaded"), Enclosure::statusToDb(Enclosure::PartiallyDownloaded));
} else {
// this should not happen
qWarning() << "Cannot find entry type" << type << "in entryCount";
}
Database::instance().execute(query);
if (!query.next())
return -1;
return query.value(0).toInt();
}
qWarning() << "Cannot find entry type" << type << "in entryCount";
return -1;
}

61
src/downloadmodel.cpp Normal file
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
*/
#include "downloadmodel.h"
#include "datamanager.h"
#include "downloadmodellogging.h"
#include "episodemodel.h"
DownloadModel::DownloadModel()
: QAbstractListModel(nullptr)
{
// initialize item counters
m_downloadingCount = DataManager::instance().entryCount(EpisodeModel::Downloading);
m_partiallyDownloadedCount = DataManager::instance().entryCount(EpisodeModel::PartiallyDownloaded);
m_downloadedCount = DataManager::instance().entryCount(EpisodeModel::Downloaded);
}
QVariant DownloadModel::data(const QModelIndex &index, int role) const
{
if (role != 0)
return QVariant();
if (index.row() < m_downloadingCount) {
return QVariant::fromValue(DataManager::instance().getEntry(EpisodeModel::Downloading, index.row()));
} else if (index.row() < m_downloadingCount + m_partiallyDownloadedCount) {
return QVariant::fromValue(DataManager::instance().getEntry(EpisodeModel::PartiallyDownloaded, index.row() - m_downloadingCount));
} else if (index.row() < m_downloadingCount + m_partiallyDownloadedCount + m_downloadedCount) {
return QVariant::fromValue(DataManager::instance().getEntry(EpisodeModel::Downloaded, index.row() - m_downloadingCount - m_partiallyDownloadedCount));
} else {
qWarning() << "Trying to fetch DownloadModel item outside of valid range; this should never happen";
return QVariant();
}
}
QHash<int, QByteArray> DownloadModel::roleNames() const
{
QHash<int, QByteArray> roleNames;
roleNames[0] = "entry";
return roleNames;
}
int DownloadModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return m_downloadingCount + m_partiallyDownloadedCount + m_downloadedCount;
}
void DownloadModel::monitorDownloadStatus()
{
beginResetModel();
m_downloadingCount = DataManager::instance().entryCount(EpisodeModel::Downloading);
m_partiallyDownloadedCount = DataManager::instance().entryCount(EpisodeModel::PartiallyDownloaded);
m_downloadedCount = DataManager::instance().entryCount(EpisodeModel::Downloaded);
endResetModel();
}

View File

@ -14,14 +14,14 @@
#include "enclosure.h"
#include "entry.h"
class DownloadProgressModel : public QAbstractListModel
class DownloadModel : public QAbstractListModel
{
Q_OBJECT
public:
static DownloadProgressModel &instance()
static DownloadModel &instance()
{
static DownloadProgressModel _instance;
static DownloadModel _instance;
return _instance;
}
@ -30,9 +30,11 @@ public:
int rowCount(const QModelIndex &parent) const override;
public Q_SLOTS:
void monitorDownloadProgress(Entry *entry, Enclosure::Status status);
void monitorDownloadStatus();
private:
explicit DownloadProgressModel();
QStringList m_entries;
explicit DownloadModel();
int m_downloadingCount = 0;
int m_partiallyDownloadedCount = 0;
int m_downloadedCount = 0;
};

View File

@ -1,53 +0,0 @@
/**
* 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 "downloadprogressmodel.h"
#include "datamanager.h"
#include "downloadprogressmodellogging.h"
DownloadProgressModel::DownloadProgressModel()
: QAbstractListModel(nullptr)
{
m_entries.clear();
}
QVariant DownloadProgressModel::data(const QModelIndex &index, int role) const
{
if (role != 0)
return QVariant();
return QVariant::fromValue(DataManager::instance().getEntry(m_entries[index.row()]));
}
QHash<int, QByteArray> DownloadProgressModel::roleNames() const
{
QHash<int, QByteArray> roleNames;
roleNames[0] = "entry";
return roleNames;
}
int DownloadProgressModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return m_entries.count();
}
void DownloadProgressModel::monitorDownloadProgress(Entry *entry, Enclosure::Status status)
{
qCDebug(kastsDownloadProgressModel) << "download status changed:" << entry->title() << status;
if (status == Enclosure::Downloading && !m_entries.contains(entry->id())) {
qCDebug(kastsDownloadProgressModel) << "inserting dowloading entry" << entry->id() << "in position" << m_entries.count();
beginInsertRows(QModelIndex(), m_entries.count(), m_entries.count());
m_entries += entry->id();
endInsertRows();
}
if (status != Enclosure::Downloading && m_entries.contains(entry->id())) {
int index = m_entries.indexOf(entry->id());
qCDebug(kastsDownloadProgressModel) << "removing dowloading entry" << entry->id() << "in position" << index;
beginRemoveRows(QModelIndex(), index, index);
m_entries.removeAt(index);
endRemoveRows();
}
}

View File

@ -12,9 +12,10 @@
#include <QNetworkReply>
#include <QSqlQuery>
#include "audiomanager.h"
#include "database.h"
#include "datamanager.h"
#include "downloadprogressmodel.h"
#include "downloadmodel.h"
#include "enclosuredownloadjob.h"
#include "entry.h"
#include "error.h"
@ -25,7 +26,7 @@ Enclosure::Enclosure(Entry *entry)
: QObject(entry)
, m_entry(entry)
{
connect(this, &Enclosure::statusChanged, &DownloadProgressModel::instance(), &DownloadProgressModel::monitorDownloadProgress);
connect(this, &Enclosure::statusChanged, &DownloadModel::instance(), &DownloadModel::monitorDownloadStatus);
connect(this, &Enclosure::downloadError, &ErrorLogModel::instance(), &ErrorLogModel::monitorErrorMessages);
connect(&Fetcher::instance(), &Fetcher::downloadFileSizeUpdated, this, [this](QString url, int fileSize, int resumedAt) {
if ((url == m_url) && ((m_size != fileSize) && (m_size != fileSize + resumedAt)) && (fileSize > 1000)) {
@ -193,6 +194,10 @@ void Enclosure::processDownloadedFile()
void Enclosure::deleteFile()
{
qCDebug(kastsEnclosure) << "Trying to delete enclosure file" << path();
if (AudioManager::instance().entry() && (m_entry == AudioManager::instance().entry())) {
qCDebug(kastsEnclosure) << "Track is still playing; let's unload it before deleting";
AudioManager::instance().setEntry(nullptr);
}
// First check if file still exists; you never know what has happened
if (QFile(path()).exists())
QFile(path()).remove();

View File

@ -34,7 +34,7 @@
#include "audiomanager.h"
#include "database.h"
#include "datamanager.h"
#include "downloadprogressmodel.h"
#include "downloadmodel.h"
#include "entriesmodel.h"
#include "entry.h"
#include "episodemodel.h"
@ -127,7 +127,7 @@ int main(int argc, char *argv[])
qmlRegisterSingletonInstance("org.kde.kasts", 1, 0, "Database", &Database::instance());
qmlRegisterSingletonInstance("org.kde.kasts", 1, 0, "DataManager", &DataManager::instance());
qmlRegisterSingletonInstance("org.kde.kasts", 1, 0, "SettingsManager", SettingsManager::self());
qmlRegisterSingletonInstance("org.kde.kasts", 1, 0, "DownloadProgressModel", &DownloadProgressModel::instance());
qmlRegisterSingletonInstance("org.kde.kasts", 1, 0, "DownloadModel", &DownloadModel::instance());
qmlRegisterSingletonInstance("org.kde.kasts", 1, 0, "ErrorLogModel", &ErrorLogModel::instance());
qmlRegisterSingletonInstance("org.kde.kasts", 1, 0, "AudioManager", &AudioManager::instance());

View File

@ -0,0 +1,72 @@
/**
* 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 org.kde.kirigami 2.15 as Kirigami
import org.kde.kasts 1.0
Kirigami.ScrollablePage {
title: i18n("Downloads")
supportsRefreshing: true
onRefreshingChanged: {
if(refreshing) {
Fetcher.fetchAll()
refreshing = false
}
}
actions.main: Kirigami.Action {
iconName: "view-refresh"
text: i18n("Refresh All Podcasts")
visible: !Kirigami.Settings.isMobile
onTriggered: Fetcher.fetchAll()
}
Kirigami.PlaceholderMessage {
visible: episodeList.count === 0
width: Kirigami.Units.gridUnit * 20
anchors.centerIn: parent
text: i18n("No Downloads")
}
Component {
id: episodeListDelegate
GenericEntryDelegate {
listView: episodeList
isDownloads: true
}
}
ListView {
id: episodeList
visible: count !== 0
model: DownloadModel
section {
delegate: Kirigami.ListSectionHeader {
height: implicitHeight // workaround for bug 422289
label: section == Enclosure.Downloading ? i18n("Downloading") :
section == Enclosure.PartiallyDownloaded ? i18n("Incomplete Downloads") :
section == Enclosure.Downloaded ? i18n("Downloaded") :
""
}
property: "entry.enclosure.status"
}
delegate: Kirigami.DelegateRecycler {
width: episodeList.width
sourceComponent: episodeListDelegate
}
}
}

View File

@ -1,83 +0,0 @@
/**
* 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 org.kde.kirigami 2.15 as Kirigami
import org.kde.kasts 1.0
Kirigami.Page {
id: page
title: i18n("Downloads")
padding: 0
actions.main: Kirigami.Action {
iconName: "view-refresh"
text: i18n("Refresh All Podcasts")
visible: !Kirigami.Settings.isMobile
onTriggered: Fetcher.fetchAll()
}
header: Loader {
id: headerLoader
active: !Kirigami.Settings.isMobile
sourceComponent: tabBarComponent
}
footer: Loader {
id: footerLoader
active: Kirigami.Settings.isMobile
sourceComponent: tabBarComponent
}
Component {
id: tabBarComponent
Controls.TabBar {
id: tabBar
position: Kirigami.Settings.isMobile ? Controls.TabBar.Footer : Controls.TabBar.Header
currentIndex: swipeView.currentIndex
contentHeight: tabBarHeight
Controls.TabButton {
width: parent.parent.width / parent.count
height: tabBarHeight
text: i18n("In Progress")
}
Controls.TabButton {
width: parent.parent.width / parent.count
height: tabBarHeight
text: i18n("Completed")
}
}
}
contentItem: Controls.SwipeView {
id: swipeView
anchors {
top: page.header.bottom
right: page.right
left: page.left
bottom: page.footer.top
}
currentIndex: Kirigami.Settings.isMobile ? footerLoader.item.currentIndex : headerLoader.item.currentIndex
EpisodeListPage {
title: i18n("In Progress")
episodeType: EpisodeModel.Downloading
}
EpisodeListPage {
title: i18n("Completed")
episodeType: EpisodeModel.Downloaded
}
}
}

View File

@ -120,8 +120,8 @@ Kirigami.ScrollablePage {
actions.right: Kirigami.Action {
text: i18n("Delete Download")
icon.name: "delete"
onTriggered: entry.enclosure.deleteFile()
visible: entry.enclosure && entry.enclosure.status === Enclosure.Downloaded && entry.queueStatus
onTriggered: entry.enclosure.deleteFile();
visible: entry.enclosure && (entry.enclosure.status === Enclosure.Downloaded || entry.enclosure.status === Enclosure.PartiallyDownloaded) && entry.queueStatus
}
contextualActions: [

View File

@ -41,8 +41,6 @@ Kirigami.ScrollablePage {
text: episodeType === EpisodeModel.All ? i18n("No Episodes Available")
: episodeType === EpisodeModel.New ? i18n("No New Episodes")
: episodeType === EpisodeModel.Unread ? i18n("No Unplayed Episodes")
: episodeType === EpisodeModel.Downloaded ? i18n("No Downloaded Episodes")
: episodeType === EpisodeModel.Downloading ? i18n("No Downloads in Progress")
: i18n("No Episodes Available")
}
@ -50,7 +48,6 @@ Kirigami.ScrollablePage {
id: episodeListDelegate
GenericEntryDelegate {
listView: episodeList
isDownloads: episodeType == EpisodeModel.Downloaded || episodeType == EpisodeModel.Downloading
}
}
@ -63,8 +60,7 @@ Kirigami.ScrollablePage {
id: episodeList
anchors.fill: parent
visible: count !== 0
model: episodeType === EpisodeModel.Downloading ? DownloadProgressModel
: episodeModel
model: episodeModel
delegate: Kirigami.DelegateRecycler {
width: episodeList.width

View File

@ -184,7 +184,7 @@ Kirigami.SwipeListItem {
entry.queueStatus = true;
entry.enclosure.download();
}
visible: !isDownloads && entry.enclosure && (entry.enclosure.status === Enclosure.Downloadable || entry.enclosure.status === Enclosure.PartiallyDownloaded)
visible: (!isDownloads || entry.enclosure.status === Enclosure.PartiallyDownloaded) && entry.enclosure && (entry.enclosure.status === Enclosure.Downloadable || entry.enclosure.status === Enclosure.PartiallyDownloaded)
},
Kirigami.Action {
text: i18n("Cancel Download")

View File

@ -42,12 +42,12 @@ Kirigami.ApplicationWindow {
tabBarActive = SettingsManager.lastOpenedPage === "FeedListPage" ? 0
: SettingsManager.lastOpenedPage === "QueuePage" ? 0
: SettingsManager.lastOpenedPage === "EpisodeSwipePage" ? 1
: SettingsManager.lastOpenedPage === "DownloadSwipePage" ? 1
: SettingsManager.lastOpenedPage === "DownloadListPage" ? 0
: 0
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"
: SettingsManager.lastOpenedPage === "DownloadListPage" ? "qrc:/DownloadListPage.qml"
: "qrc:/FeedListPage.qml")
if (SettingsManager.refreshOnStartup) Fetcher.fetchAll();
}
@ -106,10 +106,10 @@ Kirigami.ApplicationWindow {
text: i18n("Downloads")
iconName: "download"
pagePool: mainPagePool
page: "qrc:/DownloadSwipePage.qml"
page: "qrc:/DownloadListPage.qml"
onTriggered: {
SettingsManager.lastOpenedPage = "DownloadSwipePage" // for persistency
tabBarActive = 1
SettingsManager.lastOpenedPage = "DownloadListPage" // for persistency
tabBarActive = 0
}
},
Kirigami.PagePoolAction {

View File

@ -16,9 +16,9 @@
<file alias="FooterBar.qml">qml/FooterBar.qml</file>
<file alias="QueuePage.qml">qml/QueuePage.qml</file>
<file alias="EpisodeListPage.qml">qml/EpisodeListPage.qml</file>
<file alias="DownloadListPage.qml">qml/DownloadListPage.qml</file>
<file alias="ErrorListOverlay.qml">qml/ErrorListOverlay.qml</file>
<file alias="EpisodeSwipePage.qml">qml/EpisodeSwipePage.qml</file>
<file alias="DownloadSwipePage.qml">qml/DownloadSwipePage.qml</file>
<file alias="GenericHeader.qml">qml/GenericHeader.qml</file>
<file alias="GenericEntryDelegate.qml">qml/GenericEntryDelegate.qml</file>
<file alias="ImageWithFallback.qml">qml/ImageWithFallback.qml</file>