From ecdb45778e274126737aa87e27ed1b1c00cda01c Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Thu, 23 Jul 2026 07:38:28 +0000 Subject: 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 Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL --- .../main/java/org/terst/feedreader/opml/Opml.kt | 53 ++++++++++++++++------ 1 file changed, 39 insertions(+), 14 deletions(-) (limited to 'app/src/main/java/org/terst/feedreader/opml') 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 ( containing other - * 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 ( containing other + * 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() + // One entry pushed per 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() 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") -- cgit v1.2.3