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 | 65 |
1 files changed, 64 insertions, 1 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 cbad3e2..9d88374 100644 --- a/app/src/main/java/org/terst/feedreader/ui/ArticleListScreen.kt +++ b/app/src/main/java/org/terst/feedreader/ui/ArticleListScreen.kt @@ -1,5 +1,6 @@ package org.terst.feedreader.ui +import android.util.Log import androidx.activity.compose.BackHandler import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background @@ -21,6 +22,8 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable @@ -37,17 +40,24 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp +import androidx.work.WorkInfo +import androidx.work.WorkManager import coil.compose.AsyncImage +import kotlinx.coroutines.flow.catch 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 import org.terst.feedreader.data.FilterRuleEngine +import org.terst.feedreader.data.Settings import org.terst.feedreader.data.markReadBatchChunked +import org.terst.feedreader.work.FeedFetchWorker import java.text.DateFormat import java.util.Date +private const val TAG = "ArticleListScreen" + @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable fun ArticleListScreen( @@ -81,6 +91,58 @@ fun ArticleListScreen( // 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 == OLDER_FEED_ID || feedId == SAVED_FEED_ID + // A feed fetch always touches every subscription at once -- there's no such thing as + // "fetching just this one feed" or "fetching Saved." Showing "checking feed 7 of 18" + // or a new-articles snackbar on Saved (not feed-driven at all) or a single specific + // feed's screen would misleadingly imply the fetch is about whatever's being viewed. + // Scoped to the two views that actually aggregate across every subscription. + val showFetchStatus = feedId == UNREAD_FEED_ID || feedId == OLDER_FEED_ID + + val snackbarHostState = remember { SnackbarHostState() } + val settings = remember { Settings(context) } + val fetchWorkInfos by remember { + WorkManager.getInstance(context).getWorkInfosByTagFlow(FeedFetchWorker.FETCH_WORK_TAG) + // A pure status readout, not load-bearing for the article list itself -- an + // exception collected via collectAsState would otherwise propagate straight + // through Compose and crash the whole screen. Worst case here is just falling + // back to the plain "nothing's fetching" state instead of taking the app down. + .catch { e -> Log.e(TAG, "Fetch status flow failed", e); emit(emptyList()) } + }.collectAsState(initial = emptyList()) + // RUNNING only, deliberately not ENQUEUED: a periodic WorkRequest's state cycles + // straight back to ENQUEUED after every successful run rather than settling anywhere + // else, so it's ENQUEUED almost all the time between hourly runs -- treating that as + // "currently fetching" meant this permanently showed "Checking feeds…" instead of + // ever actually reaching "Up to date". + val runningFetch = fetchWorkInfos.firstOrNull { it.state == WorkInfo.State.RUNNING } + val isFetching = runningFetch != null + val fetchStatusText = runningFetch?.progress?.let { progress -> + val current = progress.getInt(FeedFetchWorker.KEY_CURRENT, -1) + val total = progress.getInt(FeedFetchWorker.KEY_TOTAL, -1) + if (current >= 0 && total > 0) "Checking feed ${current + 1} of $total…" else "Checking feeds…" + } ?: "Checking feeds…" + + // Driven by Settings' durable lastFetchAt/lastFetchNewArticles, NOT WorkInfo.SUCCEEDED + // -- a periodic fetch's WorkInfo never reaches SUCCEEDED (see above), so watching for + // that only ever caught manual/alarm-triggered one-time fetches, never the actual + // hourly background one. lastAcknowledgedFetchAt is persisted too, not just in-memory + // Compose state, so leaving and returning to this screen (a fresh composable instance) + // doesn't re-show a snackbar for a fetch that was already acknowledged elsewhere. + if (showFetchStatus) { + val lastFetchAt by settings.lastFetchAt.collectAsState(initial = 0L) + val lastFetchNewArticles by settings.lastFetchNewArticles.collectAsState(initial = 0) + val lastAcknowledgedFetchAt by settings.lastAcknowledgedFetchAt.collectAsState(initial = 0L) + LaunchedEffect(lastFetchAt) { + if (lastFetchAt > lastAcknowledgedFetchAt) { + if (lastFetchNewArticles > 0) { + snackbarHostState.showSnackbar( + if (lastFetchNewArticles == 1) "1 new article" else "$lastFetchNewArticles new articles" + ) + } + settings.acknowledgeFetch(lastFetchAt) + } + } + } + // Unread is the app's landing screen (root of the back stack) -- the system back // gesture/button here would otherwise exit the app entirely. Route it to the feed // list instead, matching the same forward affordance already in the top bar. @@ -140,7 +202,8 @@ fun ArticleListScreen( } } ) - } + }, + snackbarHost = { SnackbarHost(snackbarHostState) } ) { padding -> LazyColumn(modifier = Modifier.fillMaxSize().padding(padding)) { items(visibleArticles, key = { it.first.id }) { (article, result) -> |
