summaryrefslogtreecommitdiff
path: root/app/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java')
-rw-r--r--app/src/main/java/org/terst/feedreader/readability/ReaderExtractor.kt26
-rw-r--r--app/src/main/java/org/terst/feedreader/ui/LinkReaderScreen.kt4
-rw-r--r--app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt4
-rw-r--r--app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt17
4 files changed, 44 insertions, 7 deletions
diff --git a/app/src/main/java/org/terst/feedreader/readability/ReaderExtractor.kt b/app/src/main/java/org/terst/feedreader/readability/ReaderExtractor.kt
index c41bbfb..f9adbb4 100644
--- a/app/src/main/java/org/terst/feedreader/readability/ReaderExtractor.kt
+++ b/app/src/main/java/org/terst/feedreader/readability/ReaderExtractor.kt
@@ -32,7 +32,12 @@ data class ExtractedArticle(
* until the JS callback fires (or the timeout elapses).
*/
object ReaderExtractor {
- private const val TIMEOUT_MS = 15_000L
+ // Confirmed live: a heavy, ad/tracker-laden real page (several Ars Technica articles) can
+ // legitimately take longer than 15s to finish loading everything before onPageFinished
+ // fires, causing extraction to time out and silently fall back to no content -- not a
+ // parsing failure, just not enough runway. 25s costs little (a spinner stays up slightly
+ // longer on a truly dead page) against a real reduction in spurious extraction failures.
+ private const val TIMEOUT_MS = 25_000L
private val json = Json { ignoreUnknownKeys = true }
suspend fun extract(context: Context, url: String): ExtractedArticle? =
@@ -175,6 +180,25 @@ object ReaderExtractor {
}
}
+ // A news/blog article page's <header> conventionally holds everything
+ // BEFORE the real body: a section/category kicker, the dek/subhead, the
+ // byline, and the hero image + its caption. Readability doesn't exclude
+ // <header> by tag outright (only deprioritizes it in scoring), so on
+ // some articles enough of it scores highly enough to get pulled in
+ // alongside the real body -- confirmed live: a kicker icon, the dek
+ // paragraph, AND an orphaned duplicate hero-image caption (a mobile-only
+ // copy with no adjacent <img> at all) all leaking in above the real
+ // lede on an Ars Technica article. This is a pre-processing decision on
+ // our side, not a change to Readability's own algorithm: treat <header>
+ // as page chrome, never article content, and remove it before Readability
+ // ever sees the page. (The article's actual title is shown separately --
+ // see ReaderWebView.wrapReaderHtml -- so losing the in-body <h1> here is
+ // fine, it was never meant to double as the body text anyway.)
+ var headers = document.querySelectorAll('header');
+ for (var i = 0; i < headers.length; i++) {
+ headers[i].parentNode.removeChild(headers[i]);
+ }
+
// Readability's default hidden-element skip (treating display:none /
// visibility:hidden nodes as boilerplate) is correct for the vast
// majority of sites -- it's what keeps collapsed nav/footer/sidebar
diff --git a/app/src/main/java/org/terst/feedreader/ui/LinkReaderScreen.kt b/app/src/main/java/org/terst/feedreader/ui/LinkReaderScreen.kt
index 1a715b0..4c7251f 100644
--- a/app/src/main/java/org/terst/feedreader/ui/LinkReaderScreen.kt
+++ b/app/src/main/java/org/terst/feedreader/ui/LinkReaderScreen.kt
@@ -224,13 +224,13 @@ fun LinkReaderScreen(url: String, onBack: () -> Unit, onOpenLink: (String) -> Un
modifier = Modifier.fillMaxSize(),
factory = { ctx ->
newReaderWebView(ctx, onOpenLink).apply {
- loadDataWithBaseURL(url, wrapReaderHtml(currentExtractedContent), "text/html", "UTF-8", null)
+ loadDataWithBaseURL(url, wrapReaderHtml(currentExtractedContent, extractedTitle), "text/html", "UTF-8", null)
loadedContent.value = currentExtractedContent
}
},
update = { webView ->
if (loadedContent.value != currentExtractedContent) {
- webView.loadDataWithBaseURL(url, wrapReaderHtml(currentExtractedContent), "text/html", "UTF-8", null)
+ webView.loadDataWithBaseURL(url, wrapReaderHtml(currentExtractedContent, extractedTitle), "text/html", "UTF-8", null)
loadedContent.value = currentExtractedContent
}
},
diff --git a/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt b/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt
index 6e1f939..5ebf2ba 100644
--- a/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt
+++ b/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt
@@ -469,13 +469,13 @@ private fun ReaderContent(
modifier = Modifier.fillMaxSize(),
factory = { ctx ->
newReaderWebView(ctx, onOpenLink).apply {
- loadDataWithBaseURL(baseUrl, wrapReaderHtml(content), "text/html", "UTF-8", null)
+ loadDataWithBaseURL(baseUrl, wrapReaderHtml(content, article?.title), "text/html", "UTF-8", null)
loadedContent.value = content
}
},
update = { webView ->
if (loadedContent.value != content) {
- webView.loadDataWithBaseURL(baseUrl, wrapReaderHtml(content), "text/html", "UTF-8", null)
+ webView.loadDataWithBaseURL(baseUrl, wrapReaderHtml(content, article?.title), "text/html", "UTF-8", null)
loadedContent.value = content
}
},
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 503f71e..f95dfed 100644
--- a/app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt
+++ b/app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt
@@ -28,14 +28,27 @@ fun newReaderWebView(context: Context, onOpenLink: (String) -> Unit): WebView =
}
}
+private fun escapeHtml(text: String): String = text
+ .replace("&", "&amp;")
+ .replace("<", "&lt;")
+ .replace(">", "&gt;")
+ .replace("\"", "&quot;")
+ .replace("'", "&#39;")
+
// 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.
-fun wrapReaderHtml(bodyContent: String): String = """
+//
+// 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 <header> 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 = """
<html><head><meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { font-family: sans-serif; font-size: 18px; line-height: 1.5; padding: 16px;
background-color: #121212; color: #e1e1e1; }
+ h1.article-title { font-size: 1.5em; line-height: 1.3; margin: 0 0 0.6em 0; }
a { color: #8ab4f8; }
img { max-width: 100%; height: auto; }
svg { max-width: 100%; }
@@ -49,5 +62,5 @@ fun wrapReaderHtml(bodyContent: String): String = """
those alone -- just cap the ones that would otherwise size themselves. */
svg:not([width]):not([height]) { max-width: 32px; max-height: 32px; }
</style></head>
- <body>$bodyContent</body></html>
+ <body>${if (!title.isNullOrBlank()) "<h1 class=\"article-title\">${escapeHtml(title)}</h1>" else ""}$bodyContent</body></html>
""".trimIndent()