diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 34bb493f..2955df4f 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -91,6 +91,7 @@ else()
qt5_add_dbus_interface(SRCS dbus-interfaces/org.freedesktop.PowerManagement.Inhibit.xml inhibitinterface)
qt5_add_dbus_interface(SRCS dbus-interfaces/org.gnome.SessionManager.xml gnomesessioninterface)
+ qt5_add_dbus_interface(SRCS dbus-interfaces/org.freedesktop.NetworkManager.xml NMinterface)
endif()
add_executable(kasts ${SRCS})
diff --git a/src/dbus-interfaces/org.freedesktop.NetworkManager.xml b/src/dbus-interfaces/org.freedesktop.NetworkManager.xml
new file mode 100644
index 00000000..cebef229
--- /dev/null
+++ b/src/dbus-interfaces/org.freedesktop.NetworkManager.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/enclosure.cpp b/src/enclosure.cpp
index 0ff321f3..f32cda08 100644
--- a/src/enclosure.cpp
+++ b/src/enclosure.cpp
@@ -8,6 +8,7 @@
#include "enclosure.h"
#include "enclosurelogging.h"
+#include
#include
#include
#include
@@ -21,6 +22,7 @@
#include "error.h"
#include "errorlogmodel.h"
#include "fetcher.h"
+#include "settingsmanager.h"
Enclosure::Enclosure(Entry *entry)
: QObject(entry)
@@ -84,6 +86,15 @@ Enclosure::Status Enclosure::dbToStatus(int value)
void Enclosure::download()
{
+ if (Fetcher::instance().isMeteredConnection() && !SettingsManager::self()->allowMeteredEpisodeDownloads()) {
+ Q_EMIT downloadError(Error::Type::MeteredNotAllowed,
+ m_entry->feed()->url(),
+ m_entry->id(),
+ 0,
+ i18n("Podcast downloads not allowed due to user setting"));
+ return;
+ }
+
checkSizeOnDisk();
EnclosureDownloadJob *downloadJob = new EnclosureDownloadJob(m_url, path(), m_entry->title());
downloadJob->start();
diff --git a/src/entry.cpp b/src/entry.cpp
index f44973ac..ef65c5a3 100644
--- a/src/entry.cpp
+++ b/src/entry.cpp
@@ -217,16 +217,7 @@ QString Entry::cachedImage() const
image = m_feed->image();
}
- if (image.isEmpty()) { // this will only happen if the feed also doesn't have an image
- return QStringLiteral("no-image");
- } else {
- QString imagePath = Fetcher::instance().image(image);
- if (imagePath.isEmpty()) {
- return QStringLiteral("fetching");
- } else {
- return QStringLiteral("file://") + imagePath;
- }
- }
+ return Fetcher::instance().image(image);
}
bool Entry::queueStatus() const
diff --git a/src/feed.cpp b/src/feed.cpp
index b4e45bf7..4f977279 100644
--- a/src/feed.cpp
+++ b/src/feed.cpp
@@ -152,16 +152,7 @@ QString Feed::image() const
QString Feed::cachedImage() const
{
- if (m_image.isEmpty()) {
- return QStringLiteral("no-image");
- } else {
- QString imagePath = Fetcher::instance().image(m_image);
- if (imagePath.isEmpty()) {
- return QStringLiteral("fetching");
- } else {
- return QStringLiteral("file://") + imagePath;
- }
- }
+ return Fetcher::instance().image(m_image);
}
QString Feed::link() const
diff --git a/src/fetcher.cpp b/src/fetcher.cpp
index 583bbf35..2709b0b0 100644
--- a/src/fetcher.cpp
+++ b/src/fetcher.cpp
@@ -5,6 +5,7 @@
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
+#include
#include
#include
#include
@@ -36,6 +37,13 @@ Fetcher::Fetcher()
manager->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
manager->setStrictTransportSecurityEnabled(true);
manager->enableStrictTransportSecurityStore(true);
+
+#if !defined Q_OS_ANDROID && !defined Q_OS_WIN
+ m_nmInterface = new OrgFreedesktopNetworkManagerInterface(QStringLiteral("org.freedesktop.NetworkManager"),
+ QStringLiteral("/org/freedesktop/NetworkManager"),
+ QDBusConnection::systemBus(),
+ this);
+#endif
}
void Fetcher::fetch(const QString &url)
@@ -80,6 +88,13 @@ void Fetcher::fetchAll()
void Fetcher::retrieveFeed(const QString &url)
{
+ if (isMeteredConnection() && !SettingsManager::self()->allowMeteredFeedUpdates()) {
+ Q_EMIT error(Error::Type::MeteredNotAllowed, url, QString(), 0, i18n("Podcast updates not allowed due to user setting"));
+ m_updateProgress++;
+ Q_EMIT updateProgressChanged(m_updateProgress);
+ return;
+ }
+
qCDebug(kastsFetcher) << "Starting to fetch" << url;
Q_EMIT startedFetchingFeed(url);
@@ -346,15 +361,31 @@ void Fetcher::processEnclosure(Syndication::EnclosurePtr enclosure, Syndication:
QString Fetcher::image(const QString &url) const
{
- QString path = imagePath(url);
- if (QFileInfo::exists(path)) {
- if (QFileInfo(path).size() != 0)
- return path;
+ if (url.isEmpty()) {
+ return QLatin1String("no-image");
}
- download(url, path);
+ // if image is already cached, then return the path
+ QString path = imagePath(url);
+ if (QFileInfo::exists(path)) {
+ if (QFileInfo(path).size() != 0) {
+ return QStringLiteral("file://") + path;
+ }
+ }
- return QLatin1String("");
+ // if image has not yet been cached, then check for network connectivity if
+ // possible; and download the image
+ if (canCheckNetworkStatus()) {
+ if (networkConnected() && (!isMeteredConnection() || SettingsManager::self()->allowMeteredImageDownloads())) {
+ download(url, path);
+ } else {
+ return QLatin1String("no-image");
+ }
+ } else {
+ download(url, path);
+ }
+
+ return QLatin1String("fetching");
}
QNetworkReply *Fetcher::download(const QString &url, const QString &filePath) const
@@ -456,3 +487,43 @@ void Fetcher::setHeader(QNetworkRequest &request) const
{
request.setRawHeader("User-Agent", "Kasts/0.1; Syndication");
}
+
+bool Fetcher::canCheckNetworkStatus() const
+{
+#if !defined Q_OS_ANDROID && !defined Q_OS_WIN
+ qCDebug(kastsFetcher) << "Can NetworkManager be reached?" << m_nmInterface->isValid();
+ return (m_nmInterface && m_nmInterface->isValid());
+#else
+ return false;
+#endif
+}
+
+bool Fetcher::networkConnected() const
+{
+#if !defined Q_OS_ANDROID && !defined Q_OS_WIN
+ qCDebug(kastsFetcher) << "Network connected?" << (m_nmInterface->state() >= 70) << m_nmInterface->state();
+ return (m_nmInterface && m_nmInterface->state() >= 70);
+#else
+ return true;
+#endif
+}
+
+bool Fetcher::isMeteredConnection() const
+{
+#if !defined Q_OS_ANDROID && !defined Q_OS_WIN
+ if (canCheckNetworkStatus()) {
+ // Get network connection status through DBus (NetworkManager)
+ // state == 1: explicitly configured as metered
+ // state == 3: connection guessed as metered
+ uint state = m_nmInterface->metered();
+ qCDebug(kastsFetcher) << "Network Status:" << state;
+ qCDebug(kastsFetcher) << "Connection is metered?" << (state == 1 || state == 3);
+ return (state == 1 || state == 3);
+ } else {
+ return false;
+ }
+#else
+ // TODO: get network connection type for Android and windows
+ return false;
+#endif
+}
diff --git a/src/fetcher.h b/src/fetcher.h
index 2f3eff24..3c4ad929 100644
--- a/src/fetcher.h
+++ b/src/fetcher.h
@@ -16,6 +16,10 @@
#include "error.h"
+#if !defined Q_OS_ANDROID && !defined Q_OS_WIN
+#include "NMinterface.h"
+#endif
+
class Fetcher : public QObject
{
Q_OBJECT
@@ -41,6 +45,11 @@ public:
QString imagePath(const QString &url) const;
QString enclosurePath(const QString &url) const;
+ // Network status related methods
+ Q_INVOKABLE bool canCheckNetworkStatus() const;
+ bool networkConnected() const;
+ Q_INVOKABLE bool isMeteredConnection() const;
+
Q_SIGNALS:
void startedFetchingFeed(const QString &url);
void feedUpdated(const QString &url);
@@ -79,4 +88,8 @@ private:
int m_updateProgress;
int m_updateTotal;
bool m_updating;
+
+#if !defined Q_OS_ANDROID && !defined Q_OS_WIN
+ OrgFreedesktopNetworkManagerInterface *m_nmInterface;
+#endif
};
diff --git a/src/main.cpp b/src/main.cpp
index 99f96708..dd4bf48c 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -32,6 +32,7 @@
#include "androidlogging.h"
#endif
#include "audiomanager.h"
+#include "author.h"
#include "database.h"
#include "datamanager.h"
#include "downloadmodel.h"
diff --git a/src/qml/AddFeedSheet.qml b/src/qml/AddFeedSheet.qml
index 5fb839aa..3f44bcff 100644
--- a/src/qml/AddFeedSheet.qml
+++ b/src/qml/AddFeedSheet.qml
@@ -30,13 +30,22 @@ Kirigami.OverlaySheet {
Layout.fillWidth: true
text: "https://"
}
+
+ // This item can be used to trigger the addition of a feed; it will open an
+ // overlay with options in case the operation is not allowed by the settings
+ ConnectionCheckAction {
+ id: addFeed
+ function action() {
+ DataManager.addFeed(urlField.text)
+ }
+ }
}
footer: Controls.Button {
text: i18n("Add Podcast")
enabled: urlField.text
onClicked: {
- DataManager.addFeed(urlField.text)
+ addFeed.run()
addSheet.close()
}
}
diff --git a/src/qml/ConnectionCheckAction.qml b/src/qml/ConnectionCheckAction.qml
new file mode 100644
index 00000000..f268fabd
--- /dev/null
+++ b/src/qml/ConnectionCheckAction.qml
@@ -0,0 +1,102 @@
+/**
+ * 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 org.kde.kirigami 2.14 as Kirigami
+
+import org.kde.kasts 1.0
+
+Kirigami.OverlaySheet {
+ id: overlay
+ parent: applicationWindow().overlay
+
+ property string headingText: i18n("Podcast updates are currently not allowed on metered connections")
+ property bool condition: SettingsManager.allowMeteredFeedUpdates
+
+ // Function to be overloaded where this is instantiated with another purpose
+ // than refreshing all feeds
+ function action() {
+ Fetcher.fetchAll();
+ }
+
+ // This function will be executed when "Don't allow" is chosen; can be overloaded
+ function abortAction() { }
+
+ // This function will be executed when the "Allow once" action is chosen; can be overloaded
+ function allowOnceAction() {
+ SettingsManager.allowMeteredFeedUpdates = true;
+ action()
+ SettingsManager.allowMeteredFeedUpdates = false;
+ }
+
+ // This function will be executed when the "Always allow" action is chosed; can be overloaded
+ function alwaysAllowAction() {
+ SettingsManager.allowMeteredFeedUpdates = true;
+ action()
+ }
+
+ // this is the function that should be called if the action should be
+ // triggered conditionally (on the basis that the condition is passed)
+ function run() {
+ if (!Fetcher.isMeteredConnection() || condition) {
+ action();
+ } else {
+ overlay.open();
+ }
+ }
+
+ header: Kirigami.Heading {
+ text: overlay.headingText
+ level: 2
+ wrapMode: Text.Wrap
+ elide: Text.ElideRight
+ maximumLineCount: 3
+ }
+
+ contentItem: ColumnLayout {
+
+ spacing: 0
+
+ Kirigami.BasicListItem {
+ Layout.fillWidth: true
+ Layout.preferredHeight: Kirigami.Units.gridUnit * 2
+ leftPadding: Kirigami.Units.smallSpacing
+ rightPadding: 0
+ text: i18n("Don't Allow")
+ onClicked: {
+ abortAction();
+ close();
+ }
+ }
+
+ Kirigami.BasicListItem {
+ Layout.fillWidth: true
+ Layout.preferredHeight: Kirigami.Units.gridUnit * 2
+ leftPadding: Kirigami.Units.smallSpacing
+ rightPadding: 0
+ text: i18n("Allow Once")
+ onClicked: {
+ allowOnceAction();
+ close();
+ }
+ }
+
+ Kirigami.BasicListItem {
+ Layout.fillWidth: true
+ Layout.preferredHeight: Kirigami.Units.gridUnit * 2
+ leftPadding: Kirigami.Units.smallSpacing
+ rightPadding: 0
+ text: i18n("Always Allow")
+ onClicked: {
+ alwaysAllowAction();
+ close();
+ }
+ }
+ }
+}
diff --git a/src/qml/DownloadListPage.qml b/src/qml/DownloadListPage.qml
index 3c939fbf..a420a3f3 100644
--- a/src/qml/DownloadListPage.qml
+++ b/src/qml/DownloadListPage.qml
@@ -21,16 +21,16 @@ Kirigami.ScrollablePage {
supportsRefreshing: true
onRefreshingChanged: {
if(refreshing) {
- Fetcher.fetchAll()
- refreshing = false
+ updateAllFeeds.run();
+ refreshing = false;
}
}
actions.main: Kirigami.Action {
iconName: "view-refresh"
text: i18n("Refresh All Podcasts")
+ onTriggered: refreshing = true
visible: !Kirigami.Settings.isMobile
- onTriggered: Fetcher.fetchAll()
}
Kirigami.PlaceholderMessage {
diff --git a/src/qml/EntryListPage.qml b/src/qml/EntryListPage.qml
index ac827a47..56f215d6 100644
--- a/src/qml/EntryListPage.qml
+++ b/src/qml/EntryListPage.qml
@@ -21,11 +21,26 @@ Kirigami.ScrollablePage {
title: feed.name
supportsRefreshing: true
- onRefreshingChanged:
+ onRefreshingChanged: {
if(refreshing) {
+ updateFeed.run()
+ }
+ }
+
+ // Overlay dialog box showing options what to do on metered connections
+ ConnectionCheckAction {
+ id: updateFeed
+
+ function action() {
feed.refresh()
}
+ function abortAction() {
+ page.refreshing = false
+ }
+ }
+
+ // Make sure that this feed is also showing as "refreshing" on FeedListPage
Connections {
target: feed
function onRefreshingChanged(refreshing) {
diff --git a/src/qml/EntryPage.qml b/src/qml/EntryPage.qml
index 6553274d..42e16998 100644
--- a/src/qml/EntryPage.qml
+++ b/src/qml/EntryPage.qml
@@ -105,7 +105,8 @@ Kirigami.ScrollablePage {
if (!entry.enclosure) {
Qt.openUrlExternally(entry.link)
} else if (entry.enclosure.status === Enclosure.Downloadable || entry.enclosure.status === Enclosure.PartiallyDownloaded) {
- entry.enclosure.download()
+ downloadOverlay.entry = entry;
+ downloadOverlay.run();
} else if (entry.enclosure.status === Enclosure.Downloading) {
entry.enclosure.cancelDownload()
} else if (!entry.queueStatus) {
@@ -164,4 +165,4 @@ Kirigami.ScrollablePage {
}
}
]
-}
+}
\ No newline at end of file
diff --git a/src/qml/EpisodeListPage.qml b/src/qml/EpisodeListPage.qml
index e2966bff..09696946 100644
--- a/src/qml/EpisodeListPage.qml
+++ b/src/qml/EpisodeListPage.qml
@@ -20,8 +20,8 @@ Kirigami.ScrollablePage {
supportsRefreshing: true
onRefreshingChanged: {
if(refreshing) {
- Fetcher.fetchAll()
- refreshing = false
+ updateAllFeeds.run();
+ refreshing = false;
}
}
diff --git a/src/qml/EpisodeSwipePage.qml b/src/qml/EpisodeSwipePage.qml
index d558a92a..7cc69e96 100644
--- a/src/qml/EpisodeSwipePage.qml
+++ b/src/qml/EpisodeSwipePage.qml
@@ -22,7 +22,7 @@ Kirigami.Page {
iconName: "view-refresh"
text: i18n("Refresh All Podcasts")
visible: !Kirigami.Settings.isMobile
- onTriggered: Fetcher.fetchAll()
+ onTriggered: updateAllFeeds.run()
}
header: Loader {
diff --git a/src/qml/FeedListPage.qml b/src/qml/FeedListPage.qml
index d7f4b04f..342dd6e9 100644
--- a/src/qml/FeedListPage.qml
+++ b/src/qml/FeedListPage.qml
@@ -26,8 +26,8 @@ Kirigami.ScrollablePage {
supportsRefreshing: true
onRefreshingChanged: {
if(refreshing) {
- Fetcher.fetchAll()
- refreshing = false
+ updateAllFeeds.run();
+ refreshing = false;
}
}
diff --git a/src/qml/GenericEntryDelegate.qml b/src/qml/GenericEntryDelegate.qml
index 1e0f33b7..105ee815 100644
--- a/src/qml/GenericEntryDelegate.qml
+++ b/src/qml/GenericEntryDelegate.qml
@@ -194,8 +194,8 @@ Kirigami.SwipeListItem {
text: i18n("Download")
icon.name: "download"
onTriggered: {
- entry.queueStatus = true;
- entry.enclosure.download();
+ downloadOverlay.entry = entry;
+ downloadOverlay.run();
}
visible: (!isDownloads || entry.enclosure.status === Enclosure.PartiallyDownloaded) && entry.enclosure && (entry.enclosure.status === Enclosure.Downloadable || entry.enclosure.status === Enclosure.PartiallyDownloaded)
},
diff --git a/src/qml/QueuePage.qml b/src/qml/QueuePage.qml
index 9c1f87fe..ffaa9890 100644
--- a/src/qml/QueuePage.qml
+++ b/src/qml/QueuePage.qml
@@ -22,8 +22,8 @@ Kirigami.ScrollablePage {
supportsRefreshing: true
onRefreshingChanged: {
if(refreshing) {
- Fetcher.fetchAll()
- refreshing = false
+ updateAllFeeds.run();
+ refreshing = false;
}
}
diff --git a/src/qml/SettingsPage.qml b/src/qml/SettingsPage.qml
index d2486da8..eba22f90 100644
--- a/src/qml/SettingsPage.qml
+++ b/src/qml/SettingsPage.qml
@@ -20,7 +20,18 @@ Kirigami.ScrollablePage {
Kirigami.Heading {
Kirigami.FormData.isSection: true
+ text: i18n("Appearance")
+ }
+ Controls.CheckBox {
+ id: alwaysShowFeedTitles
+ checked: SettingsManager.alwaysShowFeedTitles
+ text: i18n("Always show podcast titles in subscription view")
+ onToggled: SettingsManager.alwaysShowFeedTitles = checked
+ }
+
+ Kirigami.Heading {
+ Kirigami.FormData.isSection: true
text: i18n("Play Settings")
}
@@ -68,15 +79,32 @@ Kirigami.ScrollablePage {
Kirigami.Heading {
Kirigami.FormData.isSection: true
- text: i18n("Appearance")
+ text: i18n("Network")
}
+ Controls.CheckBox {
+ id: allowMeteredFeedUpdates
+ checked: SettingsManager.allowMeteredFeedUpdates || !Fetcher.canCheckNetworkStatus()
+ Kirigami.FormData.label: i18n("On metered connections:")
+ text: i18n("Allow podcast updates")
+ onToggled: SettingsManager.allowMeteredFeedUpdates = checked
+ enabled: Fetcher.canCheckNetworkStatus()
+ }
Controls.CheckBox {
- id: alwaysShowFeedTitles
- checked: SettingsManager.alwaysShowFeedTitles
- text: i18n("Always show podcast titles in subscription view")
- onToggled: SettingsManager.alwaysShowFeedTitles = checked
+ id: allowMeteredEpisodeDownloads
+ checked: SettingsManager.allowMeteredEpisodeDownloads || !Fetcher.canCheckNetworkStatus()
+ text: i18n("Allow episode downloads")
+ onToggled: SettingsManager.allowMeteredEpisodeDownloads = checked
+ enabled: Fetcher.canCheckNetworkStatus()
+ }
+
+ Controls.CheckBox {
+ id: allowMeteredImageDownloads
+ checked: SettingsManager.allowMeteredImageDownloads || !Fetcher.canCheckNetworkStatus()
+ text: i18n("Allow image downloads")
+ onToggled: SettingsManager.allowMeteredImageDownloads = checked
+ enabled: Fetcher.canCheckNetworkStatus()
}
Kirigami.Heading {
diff --git a/src/qml/main.qml b/src/qml/main.qml
index 014c146e..56668f3c 100644
--- a/src/qml/main.qml
+++ b/src/qml/main.qml
@@ -58,7 +58,13 @@ Kirigami.ApplicationWindow {
: 0
currentPage = SettingsManager.lastOpenedPage
pageStack.initialPage = getPage(SettingsManager.lastOpenedPage)
- if (SettingsManager.refreshOnStartup) Fetcher.fetchAll();
+
+ // Refresh feeds on startup if allowed
+ if (SettingsManager.refreshOnStartup) {
+ if (SettingsManager.allowMeteredFeedUpdates || !Fetcher.isMeteredConnection()) {
+ Fetcher.fetchAll();
+ }
+ }
}
globalDrawer: Kirigami.GlobalDrawer {
@@ -211,6 +217,9 @@ Kirigami.ApplicationWindow {
}
+ // Notification that shows the progress of feed updates
+ // It mimicks the behaviour of an InlineMessage, because InlineMessage does
+ // not allow to add a BusyIndicator
UpdateNotification {
z: 2
id: updateNotification
@@ -222,6 +231,7 @@ Kirigami.ApplicationWindow {
}
}
+ // This InlineMessage is used for displaying error messages
ErrorNotification {
id: errorNotification
}
@@ -230,4 +240,35 @@ Kirigami.ApplicationWindow {
ErrorListOverlay {
id: errorOverlay
}
+
+ // This item can be used to trigger an update of all feeds; it will open an
+ // overlay with options in case the operation is not allowed by the settings
+ ConnectionCheckAction {
+ id: updateAllFeeds
+ }
+
+ // Overlay with options what to do when metered downloads are not allowed
+ ConnectionCheckAction {
+ id: downloadOverlay
+
+ headingText: i18n("Podcast downloads are currently not allowed on metered connections")
+ condition: SettingsManager.allowMeteredEpisodeDownloads
+ property var entry: undefined
+
+ function action() {
+ entry.queueStatus = true;
+ entry.enclosure.download();
+ }
+
+ function allowOnceAction() {
+ SettingsManager.allowMeteredEpisodeDownloads = true;
+ action();
+ SettingsManager.allowMeteredEpisodeDownloads = false;
+ }
+
+ function alwaysAllowAction() {
+ SettingsManager.allowMeteredEpisodeDownloads = true;
+ action();
+ }
+ }
}
diff --git a/src/resources.qrc b/src/resources.qrc
index 62cf8b85..f4128146 100755
--- a/src/resources.qrc
+++ b/src/resources.qrc
@@ -25,6 +25,7 @@
qml/UpdateNotification.qml
qml/HeaderBar.qml
qml/ErrorNotification.qml
+ qml/ConnectionCheckAction.qml
qtquickcontrols2.conf
../kasts.svg
diff --git a/src/settingsmanager.kcfg b/src/settingsmanager.kcfg
index 6dca4c99..59f16284 100644
--- a/src/settingsmanager.kcfg
+++ b/src/settingsmanager.kcfg
@@ -38,6 +38,20 @@
true
+
+
+
+ false
+
+
+
+ false
+
+
+
+ false
+
+