diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-07-23 06:58:50 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-07-23 06:58:50 +0000 |
| commit | 3bec692d9613b9b3e688441e5945424bd0fe20f0 (patch) | |
| tree | 3d6df74157ce0babe1ffe7b37c3d3d2a36e0390f | |
| parent | 3914ba17608884a6fa8614b9058752e0c1c0d94f (diff) | |
Drop swipe-to-next-article; bottom Previous/Next bar; smaller titles; bug report
- Dropped swipe-to-next-article entirely, per explicit instruction. Two
different fixes for the gesture interfering with WebView scrolling had
already been tried and didn't resolve the reported jerkiness (one even
broke swipe outright and was reverted) -- rather than keep tuning gesture
detection, removed the gesture altogether.
- ReaderScreen: new bottom bar with explicit Previous/Next buttons
(disabled at the ends of the article queue), replacing swipe. This is
the first slice of the "bottom action strip" backlog item. Verified
live: Previous/Next correctly page through the article queue with the
right enabled/disabled state at each end.
- Smaller title text: reader top bar title (titleMedium, was TopAppBar's
default titleLarge) and article list row titles (bodyMedium, was
ListItem's default bodyLarge).
- New "Report bug" action in the reader's overflow menu: a dialog for a
short description, which gets bundled with the article's title/URL/id,
feed id, reader-mode state, and save/Stash state into one text block
copied to the clipboard on confirm (verified live, including the
"Copied to clipboard" confirmation) -- enough to reproduce an issue in
the tungsten emulator without needing live device access.
- BACKLOG.md: marked the done slice of the bottom-strip item, added a
note on the scroll-jerkiness investigation so far (two ruled-out
theories) since swipe removal may or may not affect it -- worth a fresh
look now that nothing intercepts touches above the WebView at all.
| -rw-r--r-- | BACKLOG.md | 56 | ||||
| -rw-r--r-- | app/src/main/java/org/terst/feedreader/ui/ArticleListScreen.kt | 3 | ||||
| -rw-r--r-- | app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt | 169 |
3 files changed, 152 insertions, 76 deletions
@@ -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 |
