summaryrefslogtreecommitdiff
path: root/app/src/main/java/org/terst/feedreader/ui
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-07-23 06:58:50 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-07-23 06:58:50 +0000
commit3bec692d9613b9b3e688441e5945424bd0fe20f0 (patch)
tree3d6df74157ce0babe1ffe7b37c3d3d2a36e0390f /app/src/main/java/org/terst/feedreader/ui
parent3914ba17608884a6fa8614b9058752e0c1c0d94f (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')
-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
2 files changed, 124 insertions, 48 deletions
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