summaryrefslogtreecommitdiff
path: root/app/src/main/java/org/terst/feedreader/data/FeedParser.kt
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-07-22 16:26:45 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-07-22 16:26:45 +0000
commit52d7877fccd60a1dae33d877ba6aa765dc098a52 (patch)
treedd23bf9c42fd59dfb475759d8de969216e7b49e0 /app/src/main/java/org/terst/feedreader/data/FeedParser.kt
parent2c04d2dbef2e36025a4b795291117ad208fba411 (diff)
First round of testing feedback: images, unread landing, dark, delete
- Fix OPML import status message reporting the raw pre-dedup count (144) instead of the actual unique feed count after Room's upsert collapses duplicates (108) -- cosmetic bug, the DB was already correct. - Article thumbnails: extract media:thumbnail/enclosure or the first <img> in feed content, show via Coil in the article list. - App now lands on Unread instead of the feed list; a top-bar icon gets from there to the feed list (forward nav, not back-stack pop, since Unread is now the root). - Dark color scheme by default. - Delete-feed action (with confirmation) on each feed row -- was entirely missing. Deletes its non-saved articles too, so they don't linger as orphaned rows in the cross-feed Unread view; saved articles are kept regardless. - Room migration 1->2 for the new imageUrl column (real migration, not destructive fallback, since there's now real on-device subscription data from testing). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
Diffstat (limited to 'app/src/main/java/org/terst/feedreader/data/FeedParser.kt')
-rw-r--r--app/src/main/java/org/terst/feedreader/data/FeedParser.kt41
1 files changed, 36 insertions, 5 deletions
diff --git a/app/src/main/java/org/terst/feedreader/data/FeedParser.kt b/app/src/main/java/org/terst/feedreader/data/FeedParser.kt
index cd337d6..735a707 100644
--- a/app/src/main/java/org/terst/feedreader/data/FeedParser.kt
+++ b/app/src/main/java/org/terst/feedreader/data/FeedParser.kt
@@ -16,7 +16,8 @@ data class ParsedItem(
val link: String,
val author: String?,
val publishedAt: Long?,
- val contentHtml: String?
+ val contentHtml: String?,
+ val imageUrl: String? = null
)
data class ParsedFeed(
@@ -65,6 +66,7 @@ object FeedParser {
var itemDate: Long? = null
var itemContent: String? = null
var itemDescription: String? = null
+ var itemImageUrl: String? = null
while (eventType != XmlPullParser.END_DOCUMENT && depth > 0) {
when (eventType) {
@@ -74,7 +76,7 @@ object FeedParser {
"item" -> {
inItem = true
itemGuid = null; itemTitle = ""; itemLink = ""; itemAuthor = null
- itemDate = null; itemContent = null; itemDescription = null
+ itemDate = null; itemContent = null; itemDescription = null; itemImageUrl = null
}
"title" -> {
val text = parser.nextText()
@@ -90,19 +92,31 @@ object FeedParser {
"pubdate" -> if (inItem) itemDate = parseRfc822Date(parser.nextText())
"content:encoded" -> if (inItem) itemContent = parser.nextText()
"description" -> if (inItem) itemDescription = parser.nextText()
+ "media:thumbnail", "media:content" -> if (inItem && itemImageUrl == null) {
+ val url = parser.getAttributeValue(null, "url")
+ val type = parser.getAttributeValue(null, "type")
+ if (url != null && (type == null || type.startsWith("image/"))) itemImageUrl = url
+ }
+ "enclosure" -> if (inItem && itemImageUrl == null) {
+ val url = parser.getAttributeValue(null, "url")
+ val type = parser.getAttributeValue(null, "type")
+ if (url != null && type?.startsWith("image/") == true) itemImageUrl = url
+ }
}
}
XmlPullParser.END_TAG -> {
depth--
if (parser.name.lowercase() == "item") {
inItem = false
+ val content = itemContent ?: itemDescription
items += ParsedItem(
guid = itemGuid ?: itemLink.ifBlank { null },
title = itemTitle,
link = itemLink,
author = itemAuthor,
publishedAt = itemDate,
- contentHtml = itemContent ?: itemDescription
+ contentHtml = content,
+ imageUrl = itemImageUrl ?: extractFirstImageUrl(content)
)
}
}
@@ -127,6 +141,7 @@ object FeedParser {
var entryDate: Long? = null
var entryContent: String? = null
var entrySummary: String? = null
+ var entryImageUrl: String? = null
while (eventType != XmlPullParser.END_DOCUMENT && depth > 0) {
when (eventType) {
@@ -136,7 +151,7 @@ object FeedParser {
"entry" -> {
inEntry = true
entryId = null; entryTitle = ""; entryLink = ""; entryAuthor = null
- entryDate = null; entryContent = null; entrySummary = null
+ entryDate = null; entryContent = null; entrySummary = null; entryImageUrl = null
}
"title" -> {
val text = parser.nextText()
@@ -145,9 +160,13 @@ object FeedParser {
"link" -> {
val rel = parser.getAttributeValue(null, "rel")
val href = parser.getAttributeValue(null, "href")
+ val type = parser.getAttributeValue(null, "type")
if (href != null && (rel == null || rel == "alternate")) {
if (inEntry) entryLink = href else if (siteUrl == null) siteUrl = href
}
+ if (inEntry && rel == "enclosure" && type?.startsWith("image/") == true && href != null) {
+ entryImageUrl = href
+ }
}
"id" -> if (inEntry) entryId = parser.nextText()
"name" -> if (inEntry && entryAuthor == null) entryAuthor = parser.nextText()
@@ -155,19 +174,24 @@ object FeedParser {
"published" -> if (inEntry) entryDate = parseIso8601Date(parser.nextText()) ?: entryDate
"content" -> if (inEntry) entryContent = parser.nextText()
"summary" -> if (inEntry) entrySummary = parser.nextText()
+ "media:thumbnail" -> if (inEntry && entryImageUrl == null) {
+ entryImageUrl = parser.getAttributeValue(null, "url")
+ }
}
}
XmlPullParser.END_TAG -> {
depth--
if (parser.name.lowercase() == "entry") {
inEntry = false
+ val content = entryContent ?: entrySummary
items += ParsedItem(
guid = entryId ?: entryLink.ifBlank { null },
title = entryTitle,
link = entryLink,
author = entryAuthor,
publishedAt = entryDate,
- contentHtml = entryContent ?: entrySummary
+ contentHtml = content,
+ imageUrl = entryImageUrl ?: extractFirstImageUrl(content)
)
}
}
@@ -177,6 +201,13 @@ object FeedParser {
return ParsedFeed(feedTitle, siteUrl, items)
}
+ private val imgTagRegex = Regex("""<img[^>]+src=["']([^"']+)["']""", RegexOption.IGNORE_CASE)
+
+ // Fallback for feeds with no explicit media:thumbnail/enclosure image --
+ // most blog feeds just embed the post's lead image directly in the content.
+ private fun extractFirstImageUrl(html: String?): String? =
+ html?.let { imgTagRegex.find(it)?.groupValues?.get(1) }
+
private val rfc822Formatters = listOf(
DateTimeFormatter.RFC_1123_DATE_TIME,
DateTimeFormatterBuilder()