summaryrefslogtreecommitdiff
path: root/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-07-23 07:38:28 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-07-23 07:38:28 +0000
commitecdb45778e274126737aa87e27ed1b1c00cda01c (patch)
treeb632e53bc91475ba49094539e04b51e7a5eaa41c /app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt
parent3bec692d9613b9b3e688441e5945424bd0fe20f0 (diff)
Rebrand to FeeDreader; overflow-menu cleanup; deferred read-marking; folder-organized feed list
- Rename app to FeeDreader with an owl launcher icon and a paywall-avoidance note in Settings - Move Open original/Save to Stash into the reader's overflow menu; add Mark unread there too - Only mark an article read when navigating away via Previous/Next, not on open or on back - Saved list no longer crosses out read items; its count includes them - Back from Unread (the root screen) now opens the feed list instead of exiting the app - Feed list: clearer separator above the per-feed list, folder grouping (from OPML categories) with case-insensitive sort, and a virtual "Last fetch failed" section at the top - Reader extraction: cap unsized decorative SVG icons (fixes Ars Technica's giant section-badge icon), force native loading="lazy" images to load eagerly in the hidden extraction WebView, and drop a lightbox's duplicate caption only when a real figcaption sibling exists - Backlog notes on swipe-to-next research, extraction test coverage, and an async-rendered-gallery race confirmed live on Ars Technica Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
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.kt116
1 files changed, 73 insertions, 43 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 a46d1aa..6e1f939 100644
--- a/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt
+++ b/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt
@@ -123,11 +123,22 @@ fun ReaderScreen(
val currentPage = pagerState.currentPage
val hasPrevious = currentPage > 0
val hasNext = currentPage < safeIds.size - 1
+ // Marking read happens HERE (moving to another article), not on open -- backing out
+ // to the list without paging past an article leaves it unread. Only Next/Previous
+ // count as "moved away from it"; exiting via the back arrow deliberately doesn't.
fun goToPreviousArticle() {
- scope.launch { pagerState.animateScrollToPage((pagerState.currentPage - 1).coerceAtLeast(0)) }
+ val leavingId = safeIds[pagerState.currentPage]
+ scope.launch {
+ db.articleDao().setRead(leavingId, true)
+ pagerState.animateScrollToPage((pagerState.currentPage - 1).coerceAtLeast(0))
+ }
}
fun goToNextArticle() {
- scope.launch { pagerState.animateScrollToPage((pagerState.currentPage + 1).coerceAtMost(safeIds.size - 1)) }
+ val leavingId = safeIds[pagerState.currentPage]
+ scope.launch {
+ db.articleDao().setRead(leavingId, true)
+ pagerState.animateScrollToPage((pagerState.currentPage + 1).coerceAtMost(safeIds.size - 1))
+ }
}
HorizontalPager(state = pagerState, userScrollEnabled = false, modifier = Modifier.fillMaxSize()) { page ->
@@ -175,7 +186,8 @@ private fun ReaderContent(
LaunchedEffect(articleId) {
val loaded = db.articleDao().getById(articleId) ?: return@LaunchedEffect
article = loaded
- db.articleDao().setRead(articleId, true)
+ // Deliberately NOT marking read here -- see ReaderScreen.goToNextArticle/
+ // goToPreviousArticle, which is where "read" actually gets set now.
val loadedFeed = loaded.feedId?.let { db.feedDao().getById(it) }
feed = loadedFeed
@@ -274,16 +286,10 @@ private fun ReaderContent(
)
}
}
- IconButton(onClick = {
- val a = currentArticle ?: return@IconButton
- val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(a.link))
- context.startActivity(browserIntent)
- }) {
- Icon(Icons.Default.OpenInBrowser, contentDescription = "Open in browser")
- }
// Purely local: just flips isSaved (shows up in the Saved list). Does NOT
- // touch Stash -- that's a separate, explicit action below, since archiving
- // to Stash is a heavier/networked action the user should trigger on purpose.
+ // touch Stash -- that's a separate, explicit action in the overflow menu,
+ // since archiving to Stash is a heavier/networked action the user should
+ // trigger on purpose.
IconButton(onClick = {
val a = currentArticle ?: return@IconButton
val newSaved = !a.isSaved
@@ -295,41 +301,65 @@ private fun ReaderContent(
contentDescription = "Save locally"
)
}
- IconButton(
- enabled = !isSaving,
- onClick = {
- val a = currentArticle ?: return@IconButton
- isSaving = true
- scope.launch {
- val settings = Settings(context)
- val client = StashClient(Net.client, settings.stashBaseUrlNow(), settings.stashApiKeyNow())
- when (val result = client.saveArticle(a.link, a.title)) {
- is StashSaveResult.Saved -> {
- db.articleDao().setStashArtifactId(a.id, result.artifact.id)
- article = a.copy(stashArtifactId = result.artifact.id)
- snackbarHostState.showSnackbar("Saved to Stash")
- }
- is StashSaveResult.AlreadySaved -> {
- db.articleDao().setStashArtifactId(a.id, result.existingId)
- article = a.copy(stashArtifactId = result.existingId)
- snackbarHostState.showSnackbar("Already in Stash")
- }
- is StashSaveResult.Failed ->
- snackbarHostState.showSnackbar("Save failed: ${result.message}")
- }
- isSaving = false
- }
- }
- ) {
- Icon(
- if (currentArticle?.stashArtifactId != null) Icons.Default.CloudDone else Icons.Default.CloudUpload,
- contentDescription = "Save to Stash"
- )
- }
IconButton(onClick = { moreMenuExpanded = true }) {
Icon(Icons.Default.MoreVert, contentDescription = "More")
}
DropdownMenu(expanded = moreMenuExpanded, onDismissRequest = { moreMenuExpanded = false }) {
+ DropdownMenuItem(
+ leadingIcon = { Icon(Icons.Default.OpenInBrowser, contentDescription = null) },
+ text = { Text("Open original") },
+ onClick = {
+ moreMenuExpanded = false
+ val a = currentArticle ?: return@DropdownMenuItem
+ context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(a.link)))
+ }
+ )
+ DropdownMenuItem(
+ enabled = !isSaving,
+ leadingIcon = {
+ Icon(
+ if (currentArticle?.stashArtifactId != null) Icons.Default.CloudDone else Icons.Default.CloudUpload,
+ contentDescription = null
+ )
+ },
+ text = { Text(if (currentArticle?.stashArtifactId != null) "Saved to Stash" else "Save to Stash") },
+ onClick = {
+ moreMenuExpanded = false
+ val a = currentArticle ?: return@DropdownMenuItem
+ isSaving = true
+ scope.launch {
+ val settings = Settings(context)
+ val client = StashClient(Net.client, settings.stashBaseUrlNow(), settings.stashApiKeyNow())
+ when (val result = client.saveArticle(a.link, a.title)) {
+ is StashSaveResult.Saved -> {
+ db.articleDao().setStashArtifactId(a.id, result.artifact.id)
+ article = a.copy(stashArtifactId = result.artifact.id)
+ snackbarHostState.showSnackbar("Saved to Stash")
+ }
+ is StashSaveResult.AlreadySaved -> {
+ db.articleDao().setStashArtifactId(a.id, result.existingId)
+ article = a.copy(stashArtifactId = result.existingId)
+ snackbarHostState.showSnackbar("Already in Stash")
+ }
+ is StashSaveResult.Failed ->
+ snackbarHostState.showSnackbar("Save failed: ${result.message}")
+ }
+ isSaving = false
+ }
+ }
+ )
+ DropdownMenuItem(
+ text = { Text("Mark unread") },
+ onClick = {
+ moreMenuExpanded = false
+ val a = currentArticle ?: return@DropdownMenuItem
+ article = a.copy(isRead = false)
+ scope.launch {
+ db.articleDao().setRead(a.id, false)
+ snackbarHostState.showSnackbar("Marked unread")
+ }
+ }
+ )
// A real (JS-on) browser, not the extraction pipeline -- search results
// and login pages need actual interaction, not a Readability pass.
DropdownMenuItem(