diff options
Diffstat (limited to 'app/src/main/java/org/terst/feedreader/ui')
| -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 |
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 |
