summaryrefslogtreecommitdiff
path: root/app/src/main/java/org/terst/feedreader/opml
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-07-23 07:38:28 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-07-23 07:38:28 +0000
commitecdb45778e274126737aa87e27ed1b1c00cda01c (patch)
treeb632e53bc91475ba49094539e04b51e7a5eaa41c /app/src/main/java/org/terst/feedreader/opml
parent3bec692d9613b9b3e688441e5945424bd0fe20f0 (diff)
Rebrand to FeeDreader; overflow-menu cleanup; deferred read-marking; folder-organized feed list
- Rename app to FeeDreader with an owl launcher icon and a paywall-avoidance note in Settings - Move Open original/Save to Stash into the reader's overflow menu; add Mark unread there too - Only mark an article read when navigating away via Previous/Next, not on open or on back - Saved list no longer crosses out read items; its count includes them - Back from Unread (the root screen) now opens the feed list instead of exiting the app - Feed list: clearer separator above the per-feed list, folder grouping (from OPML categories) with case-insensitive sort, and a virtual "Last fetch failed" section at the top - Reader extraction: cap unsized decorative SVG icons (fixes Ars Technica's giant section-badge icon), force native loading="lazy" images to load eagerly in the hidden extraction WebView, and drop a lightbox's duplicate caption only when a real figcaption sibling exists - Backlog notes on swipe-to-next research, extraction test coverage, and an async-rendered-gallery race confirmed live on Ars Technica 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/opml')
-rw-r--r--app/src/main/java/org/terst/feedreader/opml/Opml.kt53
1 files changed, 39 insertions, 14 deletions
diff --git a/app/src/main/java/org/terst/feedreader/opml/Opml.kt b/app/src/main/java/org/terst/feedreader/opml/Opml.kt
index 65510fb..65ba1bf 100644
--- a/app/src/main/java/org/terst/feedreader/opml/Opml.kt
+++ b/app/src/main/java/org/terst/feedreader/opml/Opml.kt
@@ -6,13 +6,14 @@ import org.xmlpull.v1.XmlPullParser
import java.io.InputStream
import java.io.OutputStream
-data class OpmlFeed(val title: String, val xmlUrl: String, val htmlUrl: String?)
+data class OpmlFeed(val title: String, val xmlUrl: String, val htmlUrl: String?, val folder: String? = null)
/**
- * OPML import/export. Any folder nesting (<outline> containing other
- * <outline> elements instead of an xmlUrl) is flattened -- this app has no
- * folder concept, per the "simple design" scope, so a folder's contents
- * are just imported as a flat list of feeds.
+ * OPML import/export. Folder nesting (<outline> containing other <outline>
+ * elements instead of an xmlUrl) is captured as a single folder name per
+ * feed -- the nearest enclosing container outline's title/text. Nested
+ * folders-within-folders collapse to just the innermost name; this app
+ * groups by one folder level, not a full category tree.
*/
object Opml {
@@ -22,16 +23,31 @@ object Opml {
parser.setInput(input, null)
val feeds = mutableListOf<OpmlFeed>()
+ // One entry pushed per <outline> START_TAG, popped on its END_TAG -- a folder
+ // container's entry holds its name, a feed (or any other) outline's is null.
+ // The current folder is whichever named entry is nearest the top of the stack.
+ val outlineStack = mutableListOf<String?>()
var eventType = parser.eventType
while (eventType != XmlPullParser.END_DOCUMENT) {
- if (eventType == XmlPullParser.START_TAG && parser.name.equals("outline", ignoreCase = true)) {
- val xmlUrl = parser.getAttributeValue(null, "xmlUrl")
- if (xmlUrl != null) {
- val title = parser.getAttributeValue(null, "title")
- ?: parser.getAttributeValue(null, "text")
- ?: xmlUrl
- val htmlUrl = parser.getAttributeValue(null, "htmlUrl")
- feeds += OpmlFeed(title, xmlUrl, htmlUrl)
+ when (eventType) {
+ XmlPullParser.START_TAG -> if (parser.name.equals("outline", ignoreCase = true)) {
+ val xmlUrl = parser.getAttributeValue(null, "xmlUrl")
+ if (xmlUrl != null) {
+ val title = parser.getAttributeValue(null, "title")
+ ?: parser.getAttributeValue(null, "text")
+ ?: xmlUrl
+ val htmlUrl = parser.getAttributeValue(null, "htmlUrl")
+ val folder = outlineStack.filterNotNull().lastOrNull()
+ feeds += OpmlFeed(title, xmlUrl, htmlUrl, folder)
+ outlineStack += null
+ } else {
+ val folderName = parser.getAttributeValue(null, "text")
+ ?: parser.getAttributeValue(null, "title")
+ outlineStack += folderName
+ }
+ }
+ XmlPullParser.END_TAG -> if (parser.name.equals("outline", ignoreCase = true) && outlineStack.isNotEmpty()) {
+ outlineStack.removeAt(outlineStack.size - 1)
}
}
eventType = parser.next()
@@ -53,7 +69,7 @@ object Opml {
serializer.endTag(null, "head")
serializer.startTag(null, "body")
- for (feed in feeds) {
+ fun writeFeedOutline(feed: FeedEntity) {
serializer.startTag(null, "outline")
serializer.attribute(null, "text", feed.title)
serializer.attribute(null, "title", feed.title)
@@ -62,6 +78,15 @@ object Opml {
feed.siteUrl?.let { serializer.attribute(null, "htmlUrl", it) }
serializer.endTag(null, "outline")
}
+ val (grouped, ungrouped) = feeds.partition { it.folder != null }
+ for ((folder, feedsInFolder) in grouped.groupBy { it.folder }) {
+ serializer.startTag(null, "outline")
+ serializer.attribute(null, "text", folder)
+ serializer.attribute(null, "title", folder)
+ for (feed in feedsInFolder) writeFeedOutline(feed)
+ serializer.endTag(null, "outline")
+ }
+ for (feed in ungrouped) writeFeedOutline(feed)
serializer.endTag(null, "body")
serializer.endTag(null, "opml")