summaryrefslogtreecommitdiff
path: root/app/src/main/java/org/terst/feedreader/ui
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java/org/terst/feedreader/ui')
-rw-r--r--app/src/main/java/org/terst/feedreader/ui/ArticleListScreen.kt52
-rw-r--r--app/src/main/java/org/terst/feedreader/ui/FeedListScreen.kt15
-rw-r--r--app/src/main/java/org/terst/feedreader/ui/MainActivity.kt5
-rw-r--r--app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt4
4 files changed, 65 insertions, 11 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 c337492..4a67a3a 100644
--- a/app/src/main/java/org/terst/feedreader/ui/ArticleListScreen.kt
+++ b/app/src/main/java/org/terst/feedreader/ui/ArticleListScreen.kt
@@ -1,7 +1,8 @@
package org.terst.feedreader.ui
+import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
+import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@@ -12,6 +13,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.List
+import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
@@ -20,6 +22,7 @@ import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
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
@@ -27,6 +30,7 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -36,6 +40,7 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.launch
import org.terst.feedreader.data.AppDatabase
import org.terst.feedreader.data.ArticleListItem
import org.terst.feedreader.data.FilterResult
@@ -43,7 +48,7 @@ import org.terst.feedreader.data.FilterRuleEngine
import java.text.DateFormat
import java.util.Date
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable
fun ArticleListScreen(
feedId: String,
@@ -53,10 +58,13 @@ fun ArticleListScreen(
) {
val context = LocalContext.current
val db = remember { AppDatabase.get(context) }
+ val scope = rememberCoroutineScope()
+ val cutoff = remember(feedId) { System.currentTimeMillis() - RECENT_WINDOW_MS }
val articleFlow = remember(feedId) {
when (feedId) {
- UNREAD_FEED_ID -> db.articleDao().observeUnread()
+ UNREAD_FEED_ID -> db.articleDao().observeUnread(cutoff)
+ OLDER_FEED_ID -> db.articleDao().observeOlderUnread(cutoff)
SAVED_FEED_ID -> db.articleDao().observeSaved()
else -> db.articleDao().observeByFeed(feedId)
}
@@ -68,9 +76,9 @@ fun ArticleListScreen(
}
}.collectAsState(initial = emptyList())
- // Unread/Saved mix articles from every feed, so each row names its source; a
+ // Unread/Older/Saved mix articles from every feed, so each row names its source; a
// single feed's own list already says so in the top bar, so rows there don't repeat it.
- val showFeedName = feedId == UNREAD_FEED_ID || feedId == SAVED_FEED_ID
+ val showFeedName = feedId == UNREAD_FEED_ID || feedId == OLDER_FEED_ID || feedId == SAVED_FEED_ID
var specificFeedTitle by remember(feedId) { mutableStateOf<String?>(null) }
LaunchedEffect(feedId) {
@@ -79,6 +87,30 @@ fun ArticleListScreen(
}
}
+ // "Mark read from here": long-press an article to mark it and everything older
+ // (further down this same filtered/sorted list) as read in one batch, rather than
+ // opening each one individually. Confirmed rather than instant, since it can affect
+ // a lot of rows at once and there's no undo.
+ var pendingMarkReadFrom by remember(feedId) { mutableStateOf<ArticleListItem?>(null) }
+ pendingMarkReadFrom?.let { from ->
+ val index = visibleArticles.indexOfFirst { it.first.id == from.id }
+ val toMark = if (index >= 0) visibleArticles.drop(index).map { it.first }.filter { !it.isRead } else emptyList()
+ AlertDialog(
+ onDismissRequest = { pendingMarkReadFrom = null },
+ title = { Text("Mark read from here?") },
+ text = { Text("Marks ${toMark.size} article(s) as read.") },
+ confirmButton = {
+ TextButton(onClick = {
+ scope.launch { db.articleDao().markReadBatch(toMark.map { it.id }) }
+ pendingMarkReadFrom = null
+ }) { Text("Mark read") }
+ },
+ dismissButton = {
+ TextButton(onClick = { pendingMarkReadFrom = null }) { Text("Cancel") }
+ }
+ )
+ }
+
Scaffold(
topBar = {
TopAppBar(
@@ -86,6 +118,7 @@ fun ArticleListScreen(
Text(
when (feedId) {
UNREAD_FEED_ID -> "Unread"
+ OLDER_FEED_ID -> "Older"
SAVED_FEED_ID -> "Saved"
else -> specificFeedTitle ?: "Articles"
}
@@ -113,7 +146,8 @@ fun ArticleListScreen(
article = article,
highlighted = result == FilterResult.HIGHLIGHTED,
showFeedName = showFeedName,
- onClick = { onOpenArticle(article.id) }
+ onClick = { onOpenArticle(article.id) },
+ onMarkReadFrom = { pendingMarkReadFrom = article }
)
HorizontalDivider()
}
@@ -121,12 +155,14 @@ fun ArticleListScreen(
}
}
+@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun ArticleRow(
article: ArticleListItem,
highlighted: Boolean,
showFeedName: Boolean,
- onClick: () -> Unit
+ onClick: () -> Unit,
+ onMarkReadFrom: () -> Unit
) {
val dateText = article.publishedAt?.let { DateFormat.getDateInstance(DateFormat.MEDIUM).format(Date(it)) }
val metaText = listOfNotNull(if (showFeedName) article.feedTitle else null, dateText)
@@ -156,6 +192,6 @@ private fun ArticleRow(
modifier = Modifier
.fillMaxWidth()
.background(if (highlighted) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surface)
- .clickable { onClick() }
+ .combinedClickable(onClick = onClick, onLongClick = onMarkReadFrom)
)
}
diff --git a/app/src/main/java/org/terst/feedreader/ui/FeedListScreen.kt b/app/src/main/java/org/terst/feedreader/ui/FeedListScreen.kt
index 5ed5855..f8ea504 100644
--- a/app/src/main/java/org/terst/feedreader/ui/FeedListScreen.kt
+++ b/app/src/main/java/org/terst/feedreader/ui/FeedListScreen.kt
@@ -52,6 +52,7 @@ import org.terst.feedreader.work.FeedFetchWorker
fun FeedListScreen(
onOpenFeed: (String) -> Unit,
onOpenUnread: () -> Unit,
+ onOpenOlder: () -> Unit,
onOpenSaved: () -> Unit,
onAddFeed: () -> Unit,
onSettings: () -> Unit
@@ -59,6 +60,7 @@ fun FeedListScreen(
val context = LocalContext.current
val db = remember { AppDatabase.get(context) }
val scope = rememberCoroutineScope()
+ val cutoff = remember { System.currentTimeMillis() - RECENT_WINDOW_MS }
val feeds by remember {
combine(db.feedDao().observeAll(), db.articleDao().observeUnreadCounts()) { feeds, counts ->
@@ -67,7 +69,8 @@ fun FeedListScreen(
}
}.collectAsState(initial = emptyList())
- val totalUnread = feeds.sumOf { it.second }
+ val recentUnreadCount by remember { db.articleDao().observeUnreadCount(cutoff) }.collectAsState(initial = 0)
+ val olderUnreadCount by remember { db.articleDao().observeOlderUnreadCount(cutoff) }.collectAsState(initial = 0)
var menuExpanded by remember { mutableStateOf(false) }
var pendingDelete by remember { mutableStateOf<FeedEntity?>(null) }
@@ -124,10 +127,18 @@ fun FeedListScreen(
item {
ListItem(
headlineContent = { Text("Unread") },
- trailingContent = { Text(totalUnread.toString()) },
+ trailingContent = { Text(recentUnreadCount.toString()) },
modifier = Modifier.fillMaxWidth().clickable { onOpenUnread() }
)
HorizontalDivider()
+ // The rest of the unread backlog, older than RECENT_WINDOW_MS -- kept
+ // out of the main Unread view so a large backlog doesn't bury what's new.
+ ListItem(
+ headlineContent = { Text("Older") },
+ trailingContent = { Text(olderUnreadCount.toString()) },
+ modifier = Modifier.fillMaxWidth().clickable { onOpenOlder() }
+ )
+ HorizontalDivider()
ListItem(
headlineContent = { Text("Saved") },
modifier = Modifier.fillMaxWidth().clickable { onOpenSaved() }
diff --git a/app/src/main/java/org/terst/feedreader/ui/MainActivity.kt b/app/src/main/java/org/terst/feedreader/ui/MainActivity.kt
index 43e1680..0d17ab8 100644
--- a/app/src/main/java/org/terst/feedreader/ui/MainActivity.kt
+++ b/app/src/main/java/org/terst/feedreader/ui/MainActivity.kt
@@ -21,6 +21,10 @@ import java.net.URLEncoder
const val UNREAD_FEED_ID = "__unread__"
const val SAVED_FEED_ID = "__saved__"
+const val OLDER_FEED_ID = "__older__"
+
+// How far back the default Unread view reaches before an article falls into Older instead.
+const val RECENT_WINDOW_MS = 72L * 60 * 60 * 1000
fun encodeNavArg(value: String): String = URLEncoder.encode(value, "UTF-8")
fun decodeNavArg(value: String): String = URLDecoder.decode(value, "UTF-8")
@@ -55,6 +59,7 @@ fun FeedReaderApp() {
FeedListScreen(
onOpenFeed = { feedId -> navController.navigate("articles/${encodeNavArg(feedId)}") },
onOpenUnread = { navController.navigate("articles/${encodeNavArg(UNREAD_FEED_ID)}") },
+ onOpenOlder = { navController.navigate("articles/${encodeNavArg(OLDER_FEED_ID)}") },
onOpenSaved = { navController.navigate("articles/${encodeNavArg(SAVED_FEED_ID)}") },
onAddFeed = { navController.navigate("addFeed") },
onSettings = { navController.navigate("settings") }
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 eae11af..4becea6 100644
--- a/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt
+++ b/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt
@@ -84,8 +84,10 @@ fun ReaderScreen(
var articleIds by remember(feedId) { mutableStateOf<List<String>?>(null) }
LaunchedEffect(feedId) {
+ val cutoff = System.currentTimeMillis() - RECENT_WINDOW_MS
val flow = when (feedId) {
- UNREAD_FEED_ID -> db.articleDao().observeUnread()
+ UNREAD_FEED_ID -> db.articleDao().observeUnread(cutoff)
+ OLDER_FEED_ID -> db.articleDao().observeOlderUnread(cutoff)
SAVED_FEED_ID -> db.articleDao().observeSaved()
else -> db.articleDao().observeByFeed(feedId)
}