summaryrefslogtreecommitdiff
path: root/app/src/main/java/org
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java/org')
-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