[multimedia/kasts] src: Show an indicator on the delegate when episode is no longer in RSS feed

Bart De Vries <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit 3b3710d12b428ef5cfa3e21313c3622a41a112bd by Bart De Vries.
Committed on 30/07/2026 at 17:52.
Pushed by bdevries into branch 'master'.

Show an indicator on the delegate when episode is no longer in RSS feed

M  +1    -0    src/CMakeLists.txt
M  +2    -0    src/datatypes.h
M  +9    -0    src/entry.cpp
M  +4    -0    src/entry.h
M  +21   -2    src/qml/GenericEntryDelegate.qml
M  +17   -0    src/utils/updatefeedjob.cpp

https://invent.kde.org/multimedia/kasts/-/commit/3b3710d12b428ef5cfa3e21313c3622a41a112bd

diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 66f1f06e..c2f3bf20 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -400,6 +400,7 @@ if(ANDROID)
         view-sort-descending-name
         view-sort-ascending-name
         kde-symbolic
+        emblem-unmounted
     )
     ecm_add_android_apk(kasts ANDROID_DIR ${CMAKE_SOURCE_DIR}/android)
 else()
diff --git a/src/datatypes.h b/src/datatypes.h
index 3e24e35e..58776a66 100644
--- a/src/datatypes.h
+++ b/src/datatypes.h
@@ -78,6 +78,7 @@ struct EntryDetails {
     bool read;
     bool isNew;
     bool favorite;
+    bool removed;
     bool hasEnclosure;
     QString image;
     QHash<QString, AuthorDetails> authors; // key = author name
@@ -91,6 +92,7 @@ struct EntryDetails {
     int oldCreated;
     int oldUpdated;
     QString oldLink;
+    bool oldRemoved;
     bool oldHasEnclosure; // TODO: probably don't need this since there is the enclosure QHash anyway
     QString oldImage;
 };
diff --git a/src/entry.cpp b/src/entry.cpp
index b885561f..91d9784e 100644
--- a/src/entry.cpp
+++ b/src/entry.cpp
@@ -106,6 +106,10 @@ void Entry::updateFromDb(bool emitSignals)
         m_favorite = entryQuery.value(QStringLiteral("favorite")).toBool();
         Q_EMIT favoriteChanged(m_favorite);
     }
+    if (m_removed != entryQuery.value(QStringLiteral("removed")).toBool()) {
+        m_removed = entryQuery.value(QStringLiteral("removed")).toBool();
+        Q_EMIT removedChanged(m_removed);
+    }
 
     setHasEnclosure(entryQuery.value(QStringLiteral("hasEnclosure")).toBool(), emitSignals);
     setImage(entryQuery.value(QStringLiteral("image")).toString(), emitSignals);
@@ -201,6 +205,11 @@ bool Entry::favorite() const
     return m_favorite;
 }
 
+bool Entry::removed() const
+{
+    return m_removed;
+}
+
 QString Entry::baseUrl() const
 {
     return QUrl(m_link).adjusted(QUrl::RemovePath).toString();
diff --git a/src/entry.h b/src/entry.h
index d286bdf5..84337001 100644
--- a/src/entry.h
+++ b/src/entry.h
@@ -36,6 +36,7 @@ class Entry : public QObject
     Q_PROPERTY(bool read READ read WRITE setRead NOTIFY readChanged)
     Q_PROPERTY(bool new READ getNew WRITE setNew NOTIFY newChanged)
     Q_PROPERTY(bool favorite READ favorite WRITE setFavorite NOTIFY favoriteChanged)
+    Q_PROPERTY(bool removed READ removed NOTIFY removedChanged)
     Q_PROPERTY(Enclosure *enclosure READ enclosure CONSTANT)
     Q_PROPERTY(bool hasEnclosure READ hasEnclosure NOTIFY hasEnclosureChanged)
     Q_PROPERTY(QString image READ image NOTIFY imageChanged)
@@ -58,6 +59,7 @@ public:
     bool read() const;
     bool getNew() const;
     bool favorite() const;
+    bool removed() const;
     Enclosure *enclosure() const;
     bool hasEnclosure() const;
     QString image() const;
@@ -85,6 +87,7 @@ Q_SIGNALS:
     void readChanged(bool read);
     void newChanged(bool state);
     void favoriteChanged(bool favorite);
+    void removedChanged(bool removed);
     void hasEnclosureChanged(bool hasEnclosure);
     void imageChanged(const QString &url);
     void cachedImageChanged(const QString &imagePath);
@@ -114,6 +117,7 @@ private:
     bool m_read = false;
     bool m_new = false;
     bool m_favorite = false;
+    bool m_removed = false;
     Enclosure *m_enclosure = nullptr;
     QString m_image;
     bool m_hasenclosure = false;
diff --git a/src/qml/GenericEntryDelegate.qml b/src/qml/GenericEntryDelegate.qml
index 56c6376c..01dd18de 100644
--- a/src/qml/GenericEntryDelegate.qml
+++ b/src/qml/GenericEntryDelegate.qml
@@ -249,12 +249,31 @@ AddonDelegates.RoundedItemDelegate {
                 }
                 Controls.Label {
                     id: supertitle
-                    text: ((!root.isQueue && root.entry.queueStatus) || root.entry.favorite ? "·  " : "") + root.entry.updated.toLocaleDateString(Qt.locale(), Locale.NarrowFormat) + (root.entry.enclosure ? (root.entry.enclosure.size !== 0 ? "  ·  " + root.entry.enclosure.formattedSize : "") : "") + ((root.entry.feed && root.showFeedTitle) ? "  ·  " + root.entry.feed.name : "")
-                    Layout.fillWidth: true
+                    text: (((!root.isQueue && root.entry.queueStatus) || root.entry.favorite) ? "·  " : "") + root.entry.updated.toLocaleDateString(Qt.locale(), Locale.NarrowFormat) + (root.entry.enclosure ? (root.entry.enclosure.size !== 0 ? "  ·  " + root.entry.enclosure.formattedSize : "") : "") + ((root.entry.feed && root.showFeedTitle) ? "  ·  " + root.entry.feed.name : "") + (root.entry.removed ? "  ·" : "")
                     elide: Text.ElideRight
                     font: Kirigami.Theme.smallFont
                     opacity: 0.7
                 }
+                Kirigami.Icon {
+                    Layout.maximumHeight: 0.8 * supertitle.implicitHeight
+                    Layout.maximumWidth: 0.8 * supertitle.implicitHeight
+                    source: "emblem-unmounted"
+                    visible: root.entry.removed
+                    opacity: 0.7
+
+                    HoverHandler {
+                        id: removedHover
+                    }
+
+                    Controls.ToolTip.text: KI18n.i18nc("@info:tooltip Tooltip for an icon that can be displayed on an episode list item", "This episode has been removed from the podcast RSS feed by the podcast author(s). Downloading and/or streaming this episode might be broken.")
+                    Controls.ToolTip.visible: removedHover.hovered
+                    Controls.ToolTip.delay: Kirigami.Units.toolTipDelay
+                }
+                Controls.Label {
+                    text: ""
+                    Layout.fillWidth: true
+                    font: Kirigami.Theme.smallFont
+                }
             }
             Controls.Label {
                 text: root.entry.title
diff --git a/src/utils/updatefeedjob.cpp b/src/utils/updatefeedjob.cpp
index 10ad546c..00eb466d 100644
--- a/src/utils/updatefeedjob.cpp
+++ b/src/utils/updatefeedjob.cpp
@@ -216,6 +216,7 @@ void UpdateFeedJob::processFeed(const Syndication::FeedPtr feed, DataTypes::Feed
         entryDetails.read = query.value(QStringLiteral("read")).toBool();
         entryDetails.isNew = query.value(QStringLiteral("new")).toBool();
         entryDetails.link = query.value(QStringLiteral("link")).toString();
+        entryDetails.removed = query.value(QStringLiteral("removed")).toBool();
         entryDetails.hasEnclosure = query.value(QStringLiteral("hasEnclosure")).toBool();
         entryDetails.image = query.value(QStringLiteral("image")).toString();
         entryDetails.state = RecordState::Deleted; // will be set to appropriate value if the entry is found in the updated rss feed
@@ -226,6 +227,7 @@ void UpdateFeedJob::processFeed(const Syndication::FeedPtr feed, DataTypes::Feed
         entryDetails.oldCreated = entryDetails.created;
         entryDetails.oldUpdated = entryDetails.updated;
         entryDetails.oldLink = entryDetails.link;
+        entryDetails.oldRemoved = entryDetails.removed;
         entryDetails.oldHasEnclosure = entryDetails.hasEnclosure;
         entryDetails.oldImage = entryDetails.image;
 
@@ -529,6 +531,7 @@ bool UpdateFeedJob::processEntry(const Syndication::ItemPtr &entry, DataTypes::F
             updatedFeed.entries[id].created = created;
             updatedFeed.entries[id].updated = updated;
             updatedFeed.entries[id].link = link;
+            updatedFeed.entries[id].removed = false;
             updatedFeed.entries[id].hasEnclosure = hasEnclosure;
             updatedFeed.entries[id].image = image;
             updatedFeed.entries[id].state = RecordState::Modified;
@@ -845,6 +848,20 @@ void UpdateFeedJob::writeToDatabase(DataTypes::FeedDetails &updatedFeed)
     }
     writeQuery.clear();
 
+    // removed entries
+    // rather than actually remove the episodes, we mark them as such through
+    // the column "removed"
+    writeQuery.prepare(QStringLiteral("UPDATE Entries SET removed=:removed WHERE entryuid=:entryuid;"));
+    for (const EntryDetails &entryDetails : std::as_const(updatedFeed.entries)) {
+        if (entryDetails.state == RecordState::Deleted && !entryDetails.removed) {
+            updatedEntryuids.insert(entryDetails.entryuid);
+            writeQuery.bindValue(QStringLiteral(":entryuid"), entryDetails.entryuid);
+            writeQuery.bindValue(QStringLiteral(":removed"), true);
+            dbExecute(writeQuery);
+        }
+    }
+    writeQuery.clear();
+
     // new authors
     writeQuery.prepare(QStringLiteral("INSERT INTO EntryAuthors (entryuid, name, email) VALUES (:entryuid, :name, :email);"));
     for (const EntryDetails &entryDetails : std::as_const(updatedFeed.entries)) {
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.