summaryrefslogtreecommitdiff
path: root/app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-07-24 03:28:31 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-07-24 03:28:31 +0000
commit72f76cc41ae406e00a4dd9d448e2b552f9a6baaa (patch)
tree5f23e410b2ac4f8dddf89422ab57e738b3b04b2f /app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt
parentab392e2ab983a4c87e241b457c98244251b2206a (diff)
Collapsible folders, long-press link menu, YouTube thumbnails, strip more Ars sequel chrome
- Feed list: folders now collapse (start collapsed, with a per-folder unread count so collapsing doesn't hide "is there anything new") - Long-pressing a link inside an article shows an action menu (open in browser, share, save to Stash, save for later) instead of WebView's default text-selection popup -- confirmed live end to end, including the Stash/save-for-later paths creating a real standalone article - New shared ensureStandaloneArticle() helper (data/ArticleSaving.kt), factored out of ShareReceiverActivity, now also used by the long-press menu - YouTube iframe embeds become tap-to-open thumbnails (JS is off in the reader WebView, so the real player never worked anyway); tapping one goes straight to the YouTube app/browser instead of our own article-extraction pipeline - Fixed back navigation through a chain of links-within-links: each hop was stacking a new backstack entry, so back stepped through the chain one link at a time instead of returning straight to the source article. Confirmed live: A -> link -> B -> link -> C, one back-press now goes straight to A - Extended the <header> chrome-stripping from last round: also strip <footer>, an author bio card, and a "Most Read" trending-stories widget -- confirmed live one of the latter showed up as a giant unrelated photo plus an empty numbered list (its lazy-loaded items never populate in this hidden, off-screen extraction WebView) 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/ui/ReaderWebView.kt')
-rw-r--r--app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt86
1 files changed, 83 insertions, 3 deletions
diff --git a/app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt b/app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt
index f95dfed..b984d1d 100644
--- a/app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt
+++ b/app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt
@@ -1,10 +1,25 @@
package org.terst.feedreader.ui
import android.content.Context
+import android.content.Intent
+import android.net.Uri
import android.view.View
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextOverflow
+
+private fun isYouTubeWatchUrl(uri: Uri): Boolean {
+ val host = uri.host?.lowercase() ?: return false
+ return host == "youtu.be" || host.endsWith(".youtube.com") || host == "youtube.com"
+}
/**
* Shared factory for the read-only, dark-themed article WebView used by both the
@@ -12,9 +27,17 @@ import android.webkit.WebViewClient
* JS is always off -- extracted/feed HTML never needs it, and this keeps articles
* from running arbitrary scripts. Link taps are intercepted rather than left to
* WebView's default behavior (handing off to the system browser) so following a
- * link stays inside the app's own reader/extraction pipeline.
+ * link stays inside the app's own reader/extraction pipeline -- except a YouTube
+ * watch link (what ReaderExtractor turns an embedded video into a thumbnail link
+ * to), which goes straight to the YouTube app/browser instead: there's nothing for
+ * our own Readability-based reader pipeline to usefully extract from a video page.
+ *
+ * onLongPressLink fires for a long-press on a link (a plain one, or an image
+ * wrapped in one, e.g. the YouTube thumbnail) with the target URL, instead of
+ * WebView's own default long-press behavior (a native "open/copy link" popup) --
+ * callers show their own save/share/open action menu instead.
*/
-fun newReaderWebView(context: Context, onOpenLink: (String) -> Unit): WebView =
+fun newReaderWebView(context: Context, onOpenLink: (String) -> Unit, onLongPressLink: (String) -> Unit = {}): WebView =
WebView(context).apply {
settings.javaScriptEnabled = false
overScrollMode = View.OVER_SCROLL_NEVER
@@ -22,12 +45,63 @@ fun newReaderWebView(context: Context, onOpenLink: (String) -> Unit): WebView =
isHorizontalScrollBarEnabled = false
webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
- onOpenLink(request.url.toString())
+ val url = request.url
+ if (isYouTubeWatchUrl(url)) {
+ context.startActivity(Intent(Intent.ACTION_VIEW, url))
+ } else {
+ onOpenLink(url.toString())
+ }
return true
}
}
+ setOnLongClickListener {
+ val result = hitTestResult
+ val url = when (result.type) {
+ WebView.HitTestResult.SRC_ANCHOR_TYPE, WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE -> result.extra
+ else -> null
+ }
+ if (url != null) {
+ onLongPressLink(url)
+ true
+ } else {
+ false
+ }
+ }
}
+@Composable
+fun LinkActionsDialog(
+ url: String,
+ onDismiss: () -> Unit,
+ onOpenInBrowser: () -> Unit,
+ onShare: () -> Unit,
+ onSaveToStash: () -> Unit,
+ onSaveForLater: () -> Unit
+) {
+ AlertDialog(
+ onDismissRequest = onDismiss,
+ title = { Text(url, maxLines = 2, overflow = TextOverflow.Ellipsis) },
+ text = {
+ Column {
+ TextButton(onClick = { onDismiss(); onOpenInBrowser() }, modifier = Modifier.fillMaxWidth()) {
+ Text("Open in browser")
+ }
+ TextButton(onClick = { onDismiss(); onShare() }, modifier = Modifier.fillMaxWidth()) {
+ Text("Share")
+ }
+ TextButton(onClick = { onDismiss(); onSaveToStash() }, modifier = Modifier.fillMaxWidth()) {
+ Text("Save to Stash")
+ }
+ TextButton(onClick = { onDismiss(); onSaveForLater() }, modifier = Modifier.fillMaxWidth()) {
+ Text("Save for later")
+ }
+ }
+ },
+ confirmButton = {},
+ dismissButton = { TextButton(onClick = onDismiss) { Text("Cancel") } }
+ )
+}
+
private fun escapeHtml(text: String): String = text
.replace("&", "&amp;")
.replace("<", "&lt;")
@@ -61,6 +135,12 @@ fun wrapReaderHtml(bodyContent: String, title: String? = null): String = """
explicit width/height was deliberately sized by the page itself, so leave
those alone -- just cap the ones that would otherwise size themselves. */
svg:not([width]):not([height]) { max-width: 32px; max-height: 32px; }
+ /* A tap-to-open thumbnail standing in for a YouTube embed (see ReaderExtractor) --
+ JS is off here, so the real embedded player could never have worked anyway. */
+ .yt-embed { position: relative; display: block; margin: 0.5em 0; }
+ .yt-embed .yt-play { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
+ width: 0; height: 0; border-style: solid; border-width: 18px 0 18px 30px;
+ border-color: transparent transparent transparent rgba(255,255,255,0.85); }
</style></head>
<body>${if (!title.isNullOrBlank()) "<h1 class=\"article-title\">${escapeHtml(title)}</h1>" else ""}$bodyContent</body></html>
""".trimIndent()