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 * per-article ReaderScreen and LinkReaderScreen (for links tapped inside an article). * 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 -- 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, onLongPressLink: (String) -> Unit = {}): WebView = WebView(context).apply { settings.javaScriptEnabled = false overScrollMode = View.OVER_SCROLL_NEVER isVerticalScrollBarEnabled = false isHorizontalScrollBarEnabled = false webViewClient = object : WebViewClient() { override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean { 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("<", "<") .replace(">", ">") .replace("\"", """) .replace("'", "'") // Hardcoded dark palette, not @media (prefers-color-scheme) -- the app itself always // forces dark colorScheme() regardless of system setting (see MainActivity), so reader // content should match rather than following a system light/dark the app itself ignores. // // title is rendered here, as a real heading at the top of the content, rather than relying // on whatever heading Readability happened to keep in bodyContent (increasingly nothing -- // see ReaderExtractor's
stripping) or on the top app bar's title, which truncates // to one line and was the only place a long title showed up before this. fun wrapReaderHtml(bodyContent: String, title: String? = null): String = """ ${if (!title.isNullOrBlank()) "

${escapeHtml(title)}

" else ""}$bodyContent """.trimIndent()