Add support for markItemsReadOnScroll preference in TimelineTab

This commit is contained in:
Shinokuni 2024-07-18 15:24:15 +02:00
parent 8c551e748f
commit 1e5a23b722
2 changed files with 82 additions and 46 deletions

View File

@ -79,7 +79,9 @@ class TimelineScreenModel(
} }
} }
.cachedIn(screenModelScope), .cachedIn(screenModelScope),
isAccountLocal = account.isLocal isAccountLocal = account.isLocal,
lastFirstVisibleItemIndex = 0,
scrollToTop = true
) )
} }
@ -110,16 +112,21 @@ class TimelineScreenModel(
} }
screenModelScope.launch(dispatcher) { screenModelScope.launch(dispatcher) {
preferences.timelineItemSize.flow combine(
.collect { itemSize -> preferences.timelineItemSize.flow,
preferences.scrollRead.flow
) { itemSize, scrollRead -> itemSize to scrollRead }
.collect { (itemSize, scrollRead) ->
_timelineState.update { _timelineState.update {
it.copy( it.copy(
itemSize = when (itemSize) { itemSize = when (itemSize) {
"compact" -> TimelineItemSize.COMPACT "compact" -> TimelineItemSize.COMPACT
"regular" -> TimelineItemSize.REGULAR "regular" -> TimelineItemSize.REGULAR
else -> TimelineItemSize.LARGE else -> TimelineItemSize.LARGE
} },
) } markReadOnScroll = scrollRead
)
}
} }
} }
} }
@ -137,7 +144,7 @@ class TimelineScreenModel(
_timelineState.update { _timelineState.update {
it.copy( it.copy(
isRefreshing = false, isRefreshing = false,
endSynchronizing = true scrollToTop = true
) )
} }
} }
@ -196,7 +203,7 @@ class TimelineScreenModel(
_timelineState.update { _timelineState.update {
it.copy( it.copy(
isRefreshing = false, isRefreshing = false,
endSynchronizing = true, scrollToTop = true,
hideReadAllFAB = false, hideReadAllFAB = false,
localSyncErrors = if (results!!.second.isNotEmpty()) results.second else null localSyncErrors = if (results!!.second.isNotEmpty()) results.second else null
) )
@ -351,13 +358,17 @@ class TimelineScreenModel(
} }
} }
fun resetEndSynchronizing() { fun resetScrollToTop() {
_timelineState.update { it.copy(endSynchronizing = false) } _timelineState.update { it.copy(scrollToTop = false) }
} }
fun resetSyncError() { fun resetSyncError() {
_timelineState.update { it.copy(syncError = null) } _timelineState.update { it.copy(syncError = null) }
} }
fun updateLastFirstVisibleItemIndex(index: Int) {
_timelineState.update { it.copy(lastFirstVisibleItemIndex = index) }
}
} }
@Stable @Stable
@ -368,7 +379,7 @@ data class TimelineState(
val unreadNewItemsCount: Int = 0, val unreadNewItemsCount: Int = 0,
val feedCount: Int = 0, val feedCount: Int = 0,
val feedMax: Int = 0, val feedMax: Int = 0,
val endSynchronizing: Boolean = false, val scrollToTop: Boolean = false,
val localSyncErrors: ErrorResult? = null, val localSyncErrors: ErrorResult? = null,
val syncError: Exception? = null, val syncError: Exception? = null,
val filters: QueryFilters = QueryFilters(), val filters: QueryFilters = QueryFilters(),
@ -379,7 +390,9 @@ data class TimelineState(
val dialog: DialogState? = null, val dialog: DialogState? = null,
val isAccountLocal: Boolean = false, val isAccountLocal: Boolean = false,
val hideReadAllFAB: Boolean = false, val hideReadAllFAB: Boolean = false,
val itemSize: TimelineItemSize = TimelineItemSize.LARGE val itemSize: TimelineItemSize = TimelineItemSize.LARGE,
val lastFirstVisibleItemIndex: Int = 0,
val markReadOnScroll: Boolean = false
) { ) {
val showSubtitle = filters.subFilter != SubFilter.ALL val showSubtitle = filters.subFilter != SubFilter.ALL
@ -388,7 +401,7 @@ data class TimelineState(
} }
sealed interface DialogState { sealed interface DialogState {
object ConfirmDialog : DialogState data object ConfirmDialog : DialogState
object FilterSheet : DialogState data object FilterSheet : DialogState
class ErrorList(val errorResult: ErrorResult) : DialogState class ErrorList(val errorResult: ErrorResult) : DialogState
} }

View File

@ -34,6 +34,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
@ -64,6 +65,7 @@ import com.readrops.app.util.theme.spacing
import com.readrops.db.filters.ListSortType import com.readrops.db.filters.ListSortType
import com.readrops.db.filters.MainFilter import com.readrops.db.filters.MainFilter
import com.readrops.db.filters.SubFilter import com.readrops.db.filters.SubFilter
import kotlinx.coroutines.flow.filter
object TimelineTab : Tab { object TimelineTab : Tab {
@ -81,15 +83,16 @@ object TimelineTab : Tab {
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val context = LocalContext.current val context = LocalContext.current
val viewModel = getScreenModel<TimelineScreenModel>() val screenModel = getScreenModel<TimelineScreenModel>()
val state by viewModel.timelineState.collectAsStateWithLifecycle() val state by screenModel.timelineState.collectAsStateWithLifecycle()
val items = state.itemState.collectAsLazyPagingItems() val items = state.itemState.collectAsLazyPagingItems()
val lazyListState = rememberLazyListState() val lazyListState = rememberLazyListState()
val pullToRefreshState = rememberPullToRefreshState() val pullToRefreshState = rememberPullToRefreshState()
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
val topAppBarState = rememberTopAppBarState()
val topAppBarScrollBehavior = val topAppBarScrollBehavior =
TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) TopAppBarDefaults.pinnedScrollBehavior(topAppBarState)
LaunchedEffect(state.isRefreshing) { LaunchedEffect(state.isRefreshing) {
if (state.isRefreshing) { if (state.isRefreshing) {
@ -103,14 +106,15 @@ object TimelineTab : Tab {
// so we need to listen to the internal state change to trigger the refresh // so we need to listen to the internal state change to trigger the refresh
LaunchedEffect(pullToRefreshState.isRefreshing) { LaunchedEffect(pullToRefreshState.isRefreshing) {
if (pullToRefreshState.isRefreshing && !state.isRefreshing) { if (pullToRefreshState.isRefreshing && !state.isRefreshing) {
viewModel.refreshTimeline(context) screenModel.refreshTimeline(context)
} }
} }
LaunchedEffect(state.endSynchronizing) { LaunchedEffect(state.scrollToTop) {
if (state.endSynchronizing) { if (state.scrollToTop) {
lazyListState.animateScrollToItem(0) lazyListState.scrollToItem(0)
viewModel.resetEndSynchronizing() screenModel.resetScrollToTop()
topAppBarState.contentOffset = 0f
} }
} }
@ -118,9 +122,9 @@ object TimelineTab : Tab {
initialValue = DrawerValue.Closed, initialValue = DrawerValue.Closed,
confirmStateChange = { confirmStateChange = {
if (it == DrawerValue.Closed) { if (it == DrawerValue.Closed) {
viewModel.closeDrawer() screenModel.closeDrawer()
} else { } else {
viewModel.openDrawer() screenModel.openDrawer()
} }
true true
@ -129,7 +133,7 @@ object TimelineTab : Tab {
BackHandler( BackHandler(
enabled = state.isDrawerOpen, enabled = state.isDrawerOpen,
onBack = { viewModel.closeDrawer() } onBack = { screenModel.closeDrawer() }
) )
LaunchedEffect(state.isDrawerOpen) { LaunchedEffect(state.isDrawerOpen) {
@ -152,10 +156,10 @@ object TimelineTab : Tab {
) )
if (action == SnackbarResult.ActionPerformed) { if (action == SnackbarResult.ActionPerformed) {
viewModel.openDialog(DialogState.ErrorList(state.localSyncErrors!!)) screenModel.openDialog(DialogState.ErrorList(state.localSyncErrors!!))
} else { } else {
// remove errors from state // remove errors from state
viewModel.closeDialog(DialogState.ErrorList(state.localSyncErrors!!)) screenModel.closeDialog(DialogState.ErrorList(state.localSyncErrors!!))
} }
} }
} }
@ -163,7 +167,7 @@ object TimelineTab : Tab {
LaunchedEffect(state.syncError) { LaunchedEffect(state.syncError) {
if (state.syncError != null) { if (state.syncError != null) {
snackbarHostState.showSnackbar(ErrorMessage.get(state.syncError!!, context)) snackbarHostState.showSnackbar(ErrorMessage.get(state.syncError!!, context))
viewModel.resetSyncError() screenModel.resetSyncError()
} }
} }
@ -175,10 +179,10 @@ object TimelineTab : Tab {
icon = painterResource(id = R.drawable.ic_rss_feed_grey), icon = painterResource(id = R.drawable.ic_rss_feed_grey),
confirmText = "Validate", confirmText = "Validate",
dismissText = "Cancel", dismissText = "Cancel",
onDismiss = { viewModel.closeDialog() }, onDismiss = { screenModel.closeDialog() },
onConfirm = { onConfirm = {
viewModel.closeDialog() screenModel.closeDialog()
viewModel.setAllItemsRead() screenModel.setAllItemsRead()
} }
) )
} }
@ -187,24 +191,24 @@ object TimelineTab : Tab {
FilterBottomSheet( FilterBottomSheet(
filters = state.filters, filters = state.filters,
onSetShowReadItemsState = { onSetShowReadItemsState = {
viewModel.setShowReadItemsState(!state.filters.showReadItems) screenModel.setShowReadItemsState(!state.filters.showReadItems)
}, },
onSetSortTypeState = { onSetSortTypeState = {
viewModel.setSortTypeState( screenModel.setSortTypeState(
if (state.filters.sortType == ListSortType.NEWEST_TO_OLDEST) if (state.filters.sortType == ListSortType.NEWEST_TO_OLDEST)
ListSortType.OLDEST_TO_NEWEST ListSortType.OLDEST_TO_NEWEST
else else
ListSortType.NEWEST_TO_OLDEST ListSortType.NEWEST_TO_OLDEST
) )
}, },
onDismiss = { viewModel.closeDialog() } onDismiss = { screenModel.closeDialog() }
) )
} }
is DialogState.ErrorList -> { is DialogState.ErrorList -> {
ErrorListDialog( ErrorListDialog(
errorResult = dialog.errorResult, errorResult = dialog.errorResult,
onDismiss = { viewModel.closeDialog(dialog) } onDismiss = { screenModel.closeDialog(dialog) }
) )
} }
@ -217,13 +221,13 @@ object TimelineTab : Tab {
TimelineDrawer( TimelineDrawer(
state = state, state = state,
onClickDefaultItem = { onClickDefaultItem = {
viewModel.updateDrawerDefaultItem(it) screenModel.updateDrawerDefaultItem(it)
}, },
onFolderClick = { onFolderClick = {
viewModel.updateDrawerFolderSelection(it) screenModel.updateDrawerFolderSelection(it)
}, },
onFeedClick = { onFeedClick = {
viewModel.updateDrawerFeedSelection(it) screenModel.updateDrawerFeedSelection(it)
} }
) )
} }
@ -259,7 +263,7 @@ object TimelineTab : Tab {
}, },
navigationIcon = { navigationIcon = {
IconButton( IconButton(
onClick = { viewModel.openDrawer() } onClick = { screenModel.openDrawer() }
) { ) {
Icon( Icon(
imageVector = Icons.Default.Menu, imageVector = Icons.Default.Menu,
@ -269,7 +273,7 @@ object TimelineTab : Tab {
}, },
actions = { actions = {
IconButton( IconButton(
onClick = { viewModel.openDialog(DialogState.FilterSheet) } onClick = { screenModel.openDialog(DialogState.FilterSheet) }
) { ) {
Icon( Icon(
painter = painterResource(id = R.drawable.ic_filter_list), painter = painterResource(id = R.drawable.ic_filter_list),
@ -278,7 +282,7 @@ object TimelineTab : Tab {
} }
IconButton( IconButton(
onClick = { viewModel.refreshTimeline(context) } onClick = { screenModel.refreshTimeline(context) }
) { ) {
Icon( Icon(
painter = painterResource(id = R.drawable.ic_sync), painter = painterResource(id = R.drawable.ic_sync),
@ -295,9 +299,9 @@ object TimelineTab : Tab {
FloatingActionButton( FloatingActionButton(
onClick = { onClick = {
if (state.filters.mainFilter == MainFilter.ALL) { if (state.filters.mainFilter == MainFilter.ALL) {
viewModel.openDialog(DialogState.ConfirmDialog) screenModel.openDialog(DialogState.ConfirmDialog)
} else { } else {
viewModel.setAllItemsRead() screenModel.setAllItemsRead()
} }
} }
) { ) {
@ -336,9 +340,28 @@ object TimelineTab : Tab {
else -> { else -> {
if (items.itemCount > 0) { if (items.itemCount > 0) {
LaunchedEffect(Unit) {
snapshotFlow { lazyListState.firstVisibleItemIndex }
.filter { it > state.lastFirstVisibleItemIndex }
.collect {
val item = items[state.lastFirstVisibleItemIndex]!!.item
if (!item.isRead && state.markReadOnScroll) {
screenModel.setItemRead(item)
}
screenModel.updateLastFirstVisibleItemIndex(it)
}
}
LazyColumn( LazyColumn(
state = lazyListState, state = lazyListState,
contentPadding = PaddingValues(vertical = MaterialTheme.spacing.shortSpacing), contentPadding = PaddingValues(
vertical = if (state.itemSize == TimelineItemSize.COMPACT) {
0.dp
} else {
MaterialTheme.spacing.shortSpacing
}
),
verticalArrangement = Arrangement.spacedBy( verticalArrangement = Arrangement.spacedBy(
if (state.itemSize == TimelineItemSize.COMPACT) { if (state.itemSize == TimelineItemSize.COMPACT) {
0.dp 0.dp
@ -356,14 +379,14 @@ object TimelineTab : Tab {
TimelineItem( TimelineItem(
itemWithFeed = itemWithFeed, itemWithFeed = itemWithFeed,
onClick = { onClick = {
viewModel.setItemRead(itemWithFeed.item) screenModel.setItemRead(itemWithFeed.item)
navigator.push(ItemScreen(itemWithFeed.item.id)) navigator.push(ItemScreen(itemWithFeed.item.id))
}, },
onFavorite = { onFavorite = {
viewModel.updateStarState(itemWithFeed.item) screenModel.updateStarState(itemWithFeed.item)
}, },
onShare = { onShare = {
viewModel.shareItem(itemWithFeed.item, context) screenModel.shareItem(itemWithFeed.item, context)
}, },
size = state.itemSize size = state.itemSize
) )