summaryrefslogtreecommitdiff
path: root/app/src/main/java/org/terst/feedreader/opml/Opml.kt
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-07-22 08:08:58 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-07-22 08:08:58 +0000
commitd8ce4b3c0f383e844b125d2d3abd618715bb3c89 (patch)
treeb586ff9b42392f2c44645773c2a2f2aadb9db406 /app/src/main/java/org/terst/feedreader/opml/Opml.kt
Initial commit: Feeds, an Android RSS reader
Feed subscriptions (RSS 2.0 + Atom, hand-rolled parser), OPML import/export, WorkManager periodic fetch with conditional GET, Room-backed local storage (feeds/articles/filter rules), keyword mute/highlight filtering. Reader mode: on-device extraction via Mozilla's Readability.js (vendored from github.com/mozilla/readability, pinned to release 0.6.0) run in a hidden WebView, for feed articles whose provided content is too thin to be the real article body. Save: explicit action (from a feed article, or the Android share-sheet target for any URL) archives to Stash via POST /artifacts?sync=true, while a local on-device copy (Readability-extracted) is always kept so saved articles read offline regardless of Stash's reachability. 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.kt71
1 files changed, 71 insertions, 0 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
new file mode 100644
index 0000000..65510fb
--- /dev/null
+++ b/app/src/main/java/org/terst/feedreader/opml/Opml.kt
@@ -0,0 +1,71 @@
+package org.terst.feedreader.opml
+
+import android.util.Xml
+import org.terst.feedreader.data.FeedEntity
+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?)
+
+/**
+ * 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.
+ */
+object Opml {
+
+ fun parse(input: InputStream): List<OpmlFeed> {
+ val parser = Xml.newPullParser()
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
+ parser.setInput(input, null)
+
+ val feeds = mutableListOf<OpmlFeed>()
+ 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)
+ }
+ }
+ eventType = parser.next()
+ }
+ return feeds
+ }
+
+ fun write(feeds: List<FeedEntity>, output: OutputStream) {
+ val serializer = Xml.newSerializer()
+ serializer.setOutput(output, "UTF-8")
+ serializer.startDocument("UTF-8", true)
+ serializer.startTag(null, "opml")
+ serializer.attribute(null, "version", "2.0")
+
+ serializer.startTag(null, "head")
+ serializer.startTag(null, "title")
+ serializer.text("Feeds subscriptions")
+ serializer.endTag(null, "title")
+ serializer.endTag(null, "head")
+
+ serializer.startTag(null, "body")
+ for (feed in feeds) {
+ serializer.startTag(null, "outline")
+ serializer.attribute(null, "text", feed.title)
+ serializer.attribute(null, "title", feed.title)
+ serializer.attribute(null, "type", "rss")
+ serializer.attribute(null, "xmlUrl", feed.id)
+ feed.siteUrl?.let { serializer.attribute(null, "htmlUrl", it) }
+ serializer.endTag(null, "outline")
+ }
+ serializer.endTag(null, "body")
+
+ serializer.endTag(null, "opml")
+ serializer.endDocument()
+ serializer.flush()
+ }
+}