diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-07-23 07:38:28 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-07-23 07:38:28 +0000 |
| commit | ecdb45778e274126737aa87e27ed1b1c00cda01c (patch) | |
| tree | b632e53bc91475ba49094539e04b51e7a5eaa41c /app/src/main/java/org/terst/feedreader/opml/Opml.kt | |
| parent | 3bec692d9613b9b3e688441e5945424bd0fe20f0 (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/Opml.kt')
| -rw-r--r-- | app/src/main/java/org/terst/feedreader/opml/Opml.kt | 53 |
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") |
