summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--BACKLOG.md56
-rw-r--r--app/src/main/java/org/terst/feedreader/ui/ArticleListScreen.kt3
-rw-r--r--app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt169
3 files changed, 152 insertions, 76 deletions
diff --git a/BACKLOG.md b/BACKLOG.md
index b36d66c..6ed83cd 100644
--- a/BACKLOG.md
+++ b/BACKLOG.md
@@ -48,36 +48,36 @@ extracted or feed-provided HTML and swap it for a tap-to-play thumbnail
tap, rather than trying to make the embed actually play inline (which would
mean turning JS back on for untrusted third-party content).
-## Bug report button
+## Bottom action strip (partially done)
-**Motivation:** every crash/bug so far has been diagnosed by manually pulling
-logcat and querying the on-device DB over adb -- fine on tungsten's emulator,
-not available from the user's actual phone in the moment something breaks.
+**Done:** swipe-to-next-article dropped entirely (two different attempts at
+keeping it from interfering with WebView scrolling both failed to actually
+fix the reported jerkiness, and the second broke swipe outright). Replaced
+with explicit Previous/Next buttons in a new bottom bar -- no gesture to
+disambiguate with scrolling at all now.
-**Rough shape (not yet built):** a "Report bug" action (article overflow
-menu, or a dedicated crash-handler hook) that gathers what's needed to
-reproduce in the emulator -- the current article's URL/id, a short
-user-typed description, and whatever we've already instrumented internally
-(feed id, reader-mode state, isReaderMode/extraction state, recent logcat
-lines if reachable) -- and copies it to the clipboard as one block of text,
-ready to paste back into a message.
+**Still open:** the top bar is still crowded (reader-toggle, open-in-browser,
+bookmark, Stash, overflow) and top-heavy for one-handed use. Move those
+action icons + the overflow menu down into the bottom bar alongside
+Previous/Next, and auto-hide both the top and bottom bars together on
+scroll (hide on scroll-down, reveal on scroll-up or tap), matching the
+common "immersive reading" pattern other reader apps use.
-## Smaller title text
+## Scroll jerkiness (still open, root cause unknown)
-Article titles in the reader's top bar (and the list rows) read too large
-relative to everything else. Reduce the type scale -- exact size TBD by eye
-once tried.
+Reported as "jerky, loses track of my motion." Two theories tried and
+ruled out live, not just in code review:
+- An orientation-aware Compose gesture detector wrapping the WebView (for
+ swipe-to-next-article) -- kept working correctly for paging, but didn't
+ fix the reported jerkiness either, so it likely wasn't (only) about
+ gesture-arbitration overhead from that detector.
+- A passive native OnTouchListener replacing the above -- broke swipe
+ outright, reverted, and jerkiness was never re-tested in isolation since
+ swipe was still present in that build.
-## Bottom action strip
-
-**Motivation:** the reader's top bar is getting crowded (reader-toggle,
-open-in-browser, bookmark, Stash, overflow) and it's a top-heavy layout for
-a one-handed phone -- reachability, not just tidiness.
-
-**Rough shape (not yet built):** move the action icons + overflow menu into
-a bottom strip (title/back stay in the top bar). Auto-hide both the top and
-bottom strips together on scroll (hide on scroll-down, reveal on scroll-up
-or tap), matching the common "immersive reading" pattern other reader apps
-use. Needs to coexist with the existing scroll-vs-swipe gesture handling in
-ReaderScreen without reintroducing the jank/interception problems already
-fought through there.
+Swipe is now gone entirely (see "Bottom action strip" above), which removes
+one whole category of possible interference. Worth a fresh look at whether
+scrolling still feels jerky now that nothing above the WebView is doing any
+gesture detection at all -- if it still does, the cause is something else
+(WebView rendering/compositing itself, the emulator's software rendering
+not reflecting real-device feel, etc.), not gesture interception.
diff --git a/app/src/main/java/org/terst/feedreader/ui/ArticleListScreen.kt b/app/src/main/java/org/terst/feedreader/ui/ArticleListScreen.kt
index 4a67a3a..c3a398b 100644
--- a/app/src/main/java/org/terst/feedreader/ui/ArticleListScreen.kt
+++ b/app/src/main/java/org/terst/feedreader/ui/ArticleListScreen.kt
@@ -185,7 +185,8 @@ private fun ArticleRow(
headlineContent = {
Text(
article.title,
- textDecoration = if (article.isRead) TextDecoration.LineThrough else null
+ textDecoration = if (article.isRead) TextDecoration.LineThrough else null,
+ style = MaterialTheme.typography.bodyMedium
)
},
supportingContent = { if (metaText != null) Text(metaText) },
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 82cb792..a46d1aa 100644
--- a/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt
+++ b/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt
@@ -1,12 +1,18 @@
package org.terst.feedreader.ui
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
@@ -18,8 +24,12 @@ import androidx.compose.material.icons.filled.BookmarkBorder
import androidx.compose.material.icons.filled.CloudDone
import androidx.compose.material.icons.filled.CloudUpload
import androidx.compose.material.icons.filled.MoreVert
+import androidx.compose.material.icons.filled.NavigateBefore
+import androidx.compose.material.icons.filled.NavigateNext
import androidx.compose.material.icons.filled.OpenInBrowser
import androidx.compose.material.icons.outlined.Article
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
@@ -27,10 +37,12 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -41,9 +53,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
@@ -103,49 +113,34 @@ fun ReaderScreen(
val initialIndex = safeIds.indexOf(initialArticleId).coerceAtLeast(0)
val pagerState = rememberPagerState(initialPage = initialIndex) { safeIds.size }
val scope = rememberCoroutineScope()
- val swipeThresholdPx = with(LocalDensity.current) { 80.dp.toPx() }
- // Tried moving swipe detection to a passive native OnTouchListener on the WebView
- // (always returns false, never consumes) to get Compose's gesture arbitration
- // completely out of the touch path, on the theory that it was the source of reported
- // scroll jerkiness. Reverted: swipe-to-next-article stopped working at all with that
- // approach (confirmed live), which is a worse regression than unproven jerkiness.
- // Back to the known-working Compose detector -- orientation-aware touch-slop
- // detection, so it doesn't grab vertical-dominant drags, confirmed live repeatedly.
- Box(
- modifier = Modifier
- .fillMaxSize()
- .pointerInput(safeIds.size) {
- var totalDrag = 0f
- detectHorizontalDragGestures(
- onDragStart = { totalDrag = 0f },
- onDragCancel = { totalDrag = 0f },
- onDragEnd = {
- val direction = when {
- totalDrag <= -swipeThresholdPx -> 1
- totalDrag >= swipeThresholdPx -> -1
- else -> 0
- }
- if (direction != 0) {
- val target = (pagerState.currentPage + direction).coerceIn(0, safeIds.size - 1)
- scope.launch { pagerState.animateScrollToPage(target) }
- }
- },
- onHorizontalDrag = { change, dragAmount ->
- change.consume()
- totalDrag += dragAmount
- }
- )
- }
- ) {
- HorizontalPager(state = pagerState, userScrollEnabled = false, modifier = Modifier.fillMaxSize()) { page ->
- ReaderContent(
- articleId = safeIds[page],
- onBack = onBack,
- onOpenLink = onOpenLink,
- onOpenBrowser = onOpenBrowser
- )
- }
+ // Swipe-to-next-article is gone -- two different approaches to keeping it from
+ // interfering with WebView scrolling (an orientation-aware Compose gesture detector,
+ // then a passive native touch listener) were tried and neither actually resolved the
+ // reported jerkiness, and the second one broke swipe outright. Explicit Previous/Next
+ // buttons in the bottom bar replace it: no ambiguity with scrolling at all, since
+ // there's no gesture to disambiguate in the first place.
+ val currentPage = pagerState.currentPage
+ val hasPrevious = currentPage > 0
+ val hasNext = currentPage < safeIds.size - 1
+ fun goToPreviousArticle() {
+ scope.launch { pagerState.animateScrollToPage((pagerState.currentPage - 1).coerceAtLeast(0)) }
+ }
+ fun goToNextArticle() {
+ scope.launch { pagerState.animateScrollToPage((pagerState.currentPage + 1).coerceAtMost(safeIds.size - 1)) }
+ }
+
+ HorizontalPager(state = pagerState, userScrollEnabled = false, modifier = Modifier.fillMaxSize()) { page ->
+ ReaderContent(
+ articleId = safeIds[page],
+ onBack = onBack,
+ onOpenLink = onOpenLink,
+ onOpenBrowser = onOpenBrowser,
+ hasPrevious = hasPrevious,
+ hasNext = hasNext,
+ onPrevious = ::goToPreviousArticle,
+ onNext = ::goToNextArticle
+ )
}
}
@@ -155,7 +150,11 @@ private fun ReaderContent(
articleId: String,
onBack: () -> Unit,
onOpenLink: (String) -> Unit,
- onOpenBrowser: (String) -> Unit
+ onOpenBrowser: (String) -> Unit,
+ hasPrevious: Boolean,
+ hasNext: Boolean,
+ onPrevious: () -> Unit,
+ onNext: () -> Unit
) {
val context = LocalContext.current
val db = remember { AppDatabase.get(context) }
@@ -170,6 +169,8 @@ private fun ReaderContent(
var isExtracting by remember(articleId) { mutableStateOf(false) }
var isSaving by remember(articleId) { mutableStateOf(false) }
var moreMenuExpanded by remember(articleId) { mutableStateOf(false) }
+ var showBugReportDialog by remember(articleId) { mutableStateOf(false) }
+ var bugReportDescription by remember(articleId) { mutableStateOf("") }
LaunchedEffect(articleId) {
val loaded = db.articleDao().getById(articleId) ?: return@LaunchedEffect
@@ -236,7 +237,12 @@ private fun ReaderContent(
TopAppBar(
title = {
Column {
- Text(article?.title.orEmpty(), maxLines = 1, overflow = TextOverflow.Ellipsis)
+ Text(
+ article?.title.orEmpty(),
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ style = MaterialTheme.typography.titleMedium
+ )
feed?.title?.let {
Text(
it,
@@ -343,12 +349,81 @@ private fun ReaderContent(
onOpenBrowser(a.link)
}
)
+ DropdownMenuItem(
+ text = { Text("Report bug") },
+ onClick = {
+ moreMenuExpanded = false
+ showBugReportDialog = true
+ }
+ )
}
}
)
},
+ bottomBar = {
+ BottomAppBar {
+ TextButton(
+ onClick = onPrevious,
+ enabled = hasPrevious,
+ modifier = Modifier.weight(1f)
+ ) {
+ Icon(Icons.Default.NavigateBefore, contentDescription = null)
+ Text("Previous")
+ }
+ TextButton(
+ onClick = onNext,
+ enabled = hasNext,
+ modifier = Modifier.weight(1f)
+ ) {
+ Text("Next")
+ Icon(Icons.Default.NavigateNext, contentDescription = null)
+ }
+ }
+ },
snackbarHost = { SnackbarHost(snackbarHostState) }
) { padding ->
+ if (showBugReportDialog) {
+ val a = article
+ AlertDialog(
+ onDismissRequest = { showBugReportDialog = false },
+ title = { Text("Report bug") },
+ text = {
+ Column {
+ Text("Describe what happened. Copies article/feed state along with it so it can be reproduced.")
+ Spacer(modifier = Modifier.height(12.dp))
+ OutlinedTextField(
+ value = bugReportDescription,
+ onValueChange = { bugReportDescription = it },
+ label = { Text("Description") },
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ },
+ confirmButton = {
+ TextButton(onClick = {
+ val report = buildString {
+ appendLine("Feeds bug report")
+ appendLine("Description: ${bugReportDescription.ifBlank { "(none provided)" }}")
+ appendLine("Article title: ${a?.title}")
+ appendLine("Article URL: ${a?.link}")
+ appendLine("Article id: ${a?.id}")
+ appendLine("Feed: ${feed?.title} (${feed?.id})")
+ appendLine("Feed readerModeEnabled: ${feed?.readerModeEnabled}")
+ appendLine("Session isReaderMode: $isReaderMode")
+ appendLine("isSaved: ${a?.isSaved}, stashArtifactId: ${a?.stashArtifactId}")
+ appendLine("extractedContent cached: ${readerContent != null}")
+ }
+ val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ clipboard.setPrimaryClip(ClipData.newPlainText("Bug report", report))
+ showBugReportDialog = false
+ scope.launch { snackbarHostState.showSnackbar("Copied to clipboard") }
+ }) { Text("Copy") }
+ },
+ dismissButton = {
+ TextButton(onClick = { showBugReportDialog = false }) { Text("Cancel") }
+ }
+ )
+ }
Box(modifier = Modifier.fillMaxSize().padding(padding)) {
val content = if (isReaderMode) readerContent else (feedContent ?: readerContent)
val baseUrl = article?.link