mirror of https://github.com/readrops/Readrops.git
Add support for markItemsReadOnScroll preference in TimelineTab
This commit is contained in:
parent
8c551e748f
commit
1e5a23b722
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue