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 /app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt | |
| 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.
Diffstat (limited to 'app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt')
| -rw-r--r-- | app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt | 169 |
1 files changed, 122 insertions, 47 deletions
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 |
