diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-07-24 03:28:31 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-07-24 03:28:31 +0000 |
| commit | 72f76cc41ae406e00a4dd9d448e2b552f9a6baaa (patch) | |
| tree | 5f23e410b2ac4f8dddf89422ab57e738b3b04b2f /app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt | |
| parent | ab392e2ab983a4c87e241b457c98244251b2206a (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.kt | 86 |
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("&", "&") .replace("<", "<") @@ -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() |
