feat(ui): lazy tag for mark article as read
This commit is contained in:
parent
d4541dd026
commit
1343978fc9
@ -90,6 +90,7 @@ private const val TAG = "ArticleItem"
|
|||||||
fun ArticleItem(
|
fun ArticleItem(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
articleWithFeed: ArticleWithFeed,
|
articleWithFeed: ArticleWithFeed,
|
||||||
|
isUnread: Boolean = articleWithFeed.article.isUnread,
|
||||||
onClick: (ArticleWithFeed) -> Unit = {},
|
onClick: (ArticleWithFeed) -> Unit = {},
|
||||||
onLongClick: (() -> Unit)? = null
|
onLongClick: (() -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
@ -105,7 +106,7 @@ fun ArticleItem(
|
|||||||
dateString = article.dateString,
|
dateString = article.dateString,
|
||||||
imgData = article.img,
|
imgData = article.img,
|
||||||
isStarred = article.isStarred,
|
isStarred = article.isStarred,
|
||||||
isUnread = article.isUnread,
|
isUnread = isUnread,
|
||||||
onClick = { onClick(articleWithFeed) },
|
onClick = { onClick(articleWithFeed) },
|
||||||
onLongClick = onLongClick
|
onLongClick = onLongClick
|
||||||
)
|
)
|
||||||
@ -280,7 +281,7 @@ private const val SwipeActionDelay = 300L
|
|||||||
@Composable
|
@Composable
|
||||||
fun SwipeableArticleItem(
|
fun SwipeableArticleItem(
|
||||||
articleWithFeed: ArticleWithFeed,
|
articleWithFeed: ArticleWithFeed,
|
||||||
isFilterUnread: Boolean = false,
|
isUnread: Boolean = articleWithFeed.article.isUnread,
|
||||||
articleListTonalElevation: Int = 0,
|
articleListTonalElevation: Int = 0,
|
||||||
onClick: (ArticleWithFeed) -> Unit = {},
|
onClick: (ArticleWithFeed) -> Unit = {},
|
||||||
isSwipeEnabled: () -> Boolean = { false },
|
isSwipeEnabled: () -> Boolean = { false },
|
||||||
@ -311,7 +312,7 @@ fun SwipeableArticleItem(
|
|||||||
|
|
||||||
SwipeActionBox(
|
SwipeActionBox(
|
||||||
articleWithFeed = articleWithFeed,
|
articleWithFeed = articleWithFeed,
|
||||||
isRead = !articleWithFeed.article.isUnread,
|
isRead = !isUnread,
|
||||||
isStarred = articleWithFeed.article.isStarred,
|
isStarred = articleWithFeed.article.isStarred,
|
||||||
onToggleStarred = onToggleStarred,
|
onToggleStarred = onToggleStarred,
|
||||||
onToggleRead = onToggleRead
|
onToggleRead = onToggleRead
|
||||||
@ -337,6 +338,7 @@ fun SwipeableArticleItem(
|
|||||||
) {
|
) {
|
||||||
ArticleItem(
|
ArticleItem(
|
||||||
articleWithFeed = articleWithFeed,
|
articleWithFeed = articleWithFeed,
|
||||||
|
isUnread = isUnread,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick
|
onLongClick = onLongClick
|
||||||
)
|
)
|
||||||
|
@ -16,7 +16,7 @@ import me.ash.reader.domain.model.article.ArticleWithFeed
|
|||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
fun LazyListScope.ArticleList(
|
fun LazyListScope.ArticleList(
|
||||||
pagingItems: LazyPagingItems<ArticleFlowItem>,
|
pagingItems: LazyPagingItems<ArticleFlowItem>,
|
||||||
isFilterUnread: Boolean,
|
diffMap: Map<String, Diff>,
|
||||||
isShowFeedIcon: Boolean,
|
isShowFeedIcon: Boolean,
|
||||||
isShowStickyHeader: Boolean,
|
isShowStickyHeader: Boolean,
|
||||||
articleListTonalElevation: Int,
|
articleListTonalElevation: Int,
|
||||||
@ -40,9 +40,10 @@ fun LazyListScope.ArticleList(
|
|||||||
) { index ->
|
) { index ->
|
||||||
when (val item = pagingItems[index]) {
|
when (val item = pagingItems[index]) {
|
||||||
is ArticleFlowItem.Article -> {
|
is ArticleFlowItem.Article -> {
|
||||||
|
val article = item.articleWithFeed.article
|
||||||
SwipeableArticleItem(
|
SwipeableArticleItem(
|
||||||
articleWithFeed = item.articleWithFeed,
|
articleWithFeed = item.articleWithFeed,
|
||||||
isFilterUnread = isFilterUnread,
|
isUnread = diffMap[article.id]?.isUnread ?: article.isUnread,
|
||||||
articleListTonalElevation = articleListTonalElevation,
|
articleListTonalElevation = articleListTonalElevation,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
isSwipeEnabled = isSwipeEnabled,
|
isSwipeEnabled = isSwipeEnabled,
|
||||||
@ -70,9 +71,10 @@ fun LazyListScope.ArticleList(
|
|||||||
when (val item = pagingItems.peek(index)) {
|
when (val item = pagingItems.peek(index)) {
|
||||||
is ArticleFlowItem.Article -> {
|
is ArticleFlowItem.Article -> {
|
||||||
item(key = key(item), contentType = contentType(item)) {
|
item(key = key(item), contentType = contentType(item)) {
|
||||||
|
val article = item.articleWithFeed.article
|
||||||
SwipeableArticleItem(
|
SwipeableArticleItem(
|
||||||
articleWithFeed = item.articleWithFeed,
|
articleWithFeed = item.articleWithFeed,
|
||||||
isFilterUnread = isFilterUnread,
|
isUnread = diffMap[article.id]?.isUnread ?: article.isUnread,
|
||||||
articleListTonalElevation = articleListTonalElevation,
|
articleListTonalElevation = articleListTonalElevation,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
isSwipeEnabled = isSwipeEnabled,
|
isSwipeEnabled = isSwipeEnabled,
|
||||||
|
@ -32,6 +32,7 @@ import androidx.compose.ui.focus.FocusRequester
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
|
import androidx.compose.ui.res.dimensionResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
@ -43,7 +44,6 @@ import kotlinx.coroutines.launch
|
|||||||
import me.ash.reader.R
|
import me.ash.reader.R
|
||||||
import me.ash.reader.domain.model.article.ArticleWithFeed
|
import me.ash.reader.domain.model.article.ArticleWithFeed
|
||||||
import me.ash.reader.domain.model.general.Filter
|
import me.ash.reader.domain.model.general.Filter
|
||||||
import me.ash.reader.domain.model.general.MarkAsReadConditions
|
|
||||||
import me.ash.reader.infrastructure.preference.LocalFlowArticleListDateStickyHeader
|
import me.ash.reader.infrastructure.preference.LocalFlowArticleListDateStickyHeader
|
||||||
import me.ash.reader.infrastructure.preference.LocalFlowArticleListFeedIcon
|
import me.ash.reader.infrastructure.preference.LocalFlowArticleListFeedIcon
|
||||||
import me.ash.reader.infrastructure.preference.LocalFlowArticleListTonalElevation
|
import me.ash.reader.infrastructure.preference.LocalFlowArticleListTonalElevation
|
||||||
@ -107,6 +107,12 @@ fun FlowPage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DisposableEffect(pagingItems) {
|
||||||
|
onDispose {
|
||||||
|
flowViewModel.commitDiff()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
DisposableEffect(owner) {
|
DisposableEffect(owner) {
|
||||||
homeViewModel.syncWorkLiveData.observe(owner) { workInfoList ->
|
homeViewModel.syncWorkLiveData.observe(owner) { workInfoList ->
|
||||||
workInfoList.let {
|
workInfoList.let {
|
||||||
@ -127,15 +133,16 @@ fun FlowPage(
|
|||||||
|
|
||||||
val onToggleRead: (ArticleWithFeed) -> Unit = remember {
|
val onToggleRead: (ArticleWithFeed) -> Unit = remember {
|
||||||
{ article ->
|
{ article ->
|
||||||
flowViewModel.updateReadStatus(
|
val id = article.article.id
|
||||||
groupId = null,
|
val isUnread = article.article.isUnread
|
||||||
feedId = null,
|
|
||||||
articleId = article.article.id,
|
with(flowViewModel.diffMap) {
|
||||||
conditions = MarkAsReadConditions.All,
|
if (contains(id)) remove(id)
|
||||||
isUnread = !article.article.isUnread,
|
else put(id, Diff(isUnread = !isUnread))
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val onMarkAboveAsRead: ((ArticleWithFeed) -> Unit)? = remember {
|
val onMarkAboveAsRead: ((ArticleWithFeed) -> Unit)? = remember {
|
||||||
{
|
{
|
||||||
flowViewModel.markAsReadFromListByDate(
|
flowViewModel.markAsReadFromListByDate(
|
||||||
@ -326,7 +333,7 @@ fun FlowPage(
|
|||||||
}
|
}
|
||||||
ArticleList(
|
ArticleList(
|
||||||
pagingItems = pagingItems,
|
pagingItems = pagingItems,
|
||||||
isFilterUnread = filterUiState.filter == Filter.Unread,
|
diffMap = flowViewModel.diffMap,
|
||||||
isShowFeedIcon = articleListFeedIcon.value,
|
isShowFeedIcon = articleListFeedIcon.value,
|
||||||
isShowStickyHeader = articleListDateStickyHeader.value,
|
isShowStickyHeader = articleListDateStickyHeader.value,
|
||||||
articleListTonalElevation = articleListTonalElevation.value,
|
articleListTonalElevation = articleListTonalElevation.value,
|
||||||
|
@ -1,24 +1,22 @@
|
|||||||
package me.ash.reader.ui.page.home.flow
|
package me.ash.reader.ui.page.home.flow
|
||||||
|
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.runtime.mutableStateMapOf
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.paging.compose.LazyPagingItems
|
import androidx.paging.compose.LazyPagingItems
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.ash.reader.domain.model.article.ArticleFlowItem
|
import me.ash.reader.domain.model.article.ArticleFlowItem
|
||||||
import me.ash.reader.domain.model.article.ArticleWithFeed
|
|
||||||
import me.ash.reader.domain.model.general.MarkAsReadConditions
|
import me.ash.reader.domain.model.general.MarkAsReadConditions
|
||||||
import me.ash.reader.domain.service.RssService
|
import me.ash.reader.domain.service.RssService
|
||||||
import me.ash.reader.infrastructure.di.ApplicationScope
|
import me.ash.reader.infrastructure.di.ApplicationScope
|
||||||
import me.ash.reader.infrastructure.di.IODispatcher
|
import me.ash.reader.infrastructure.di.IODispatcher
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.function.BiPredicate
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
@ -32,6 +30,7 @@ class FlowViewModel @Inject constructor(
|
|||||||
|
|
||||||
private val _flowUiState = MutableStateFlow(FlowUiState())
|
private val _flowUiState = MutableStateFlow(FlowUiState())
|
||||||
val flowUiState: StateFlow<FlowUiState> = _flowUiState.asStateFlow()
|
val flowUiState: StateFlow<FlowUiState> = _flowUiState.asStateFlow()
|
||||||
|
val diffMap = mutableStateMapOf<String, Diff>()
|
||||||
|
|
||||||
fun sync() {
|
fun sync() {
|
||||||
applicationScope.launch(ioDispatcher) {
|
applicationScope.launch(ioDispatcher) {
|
||||||
@ -45,10 +44,8 @@ class FlowViewModel @Inject constructor(
|
|||||||
articleId: String?,
|
articleId: String?,
|
||||||
conditions: MarkAsReadConditions,
|
conditions: MarkAsReadConditions,
|
||||||
isUnread: Boolean,
|
isUnread: Boolean,
|
||||||
withDelay: Long = 0,
|
|
||||||
) {
|
) {
|
||||||
applicationScope.launch(ioDispatcher) {
|
applicationScope.launch(ioDispatcher) {
|
||||||
delay(withDelay)
|
|
||||||
rssService.get().markAsRead(
|
rssService.get().markAsRead(
|
||||||
groupId = groupId,
|
groupId = groupId,
|
||||||
feedId = feedId,
|
feedId = feedId,
|
||||||
@ -62,11 +59,8 @@ class FlowViewModel @Inject constructor(
|
|||||||
fun updateStarredStatus(
|
fun updateStarredStatus(
|
||||||
articleId: String?,
|
articleId: String?,
|
||||||
isStarred: Boolean,
|
isStarred: Boolean,
|
||||||
withDelay: Long = 0,
|
|
||||||
) {
|
) {
|
||||||
applicationScope.launch(ioDispatcher) {
|
applicationScope.launch(ioDispatcher) {
|
||||||
// FIXME: a dirty hack to ensure the swipe animation doesn't get interrupted when recomposed, remove this after implementing a lazy tag!
|
|
||||||
delay(withDelay)
|
|
||||||
if (articleId != null) {
|
if (articleId != null) {
|
||||||
rssService.get().markAsStarred(
|
rssService.get().markAsStarred(
|
||||||
articleId = articleId,
|
articleId = articleId,
|
||||||
@ -97,6 +91,21 @@ class FlowViewModel @Inject constructor(
|
|||||||
rssService.get().batchMarkAsRead(articleIds = articleIdSet, isUnread = false)
|
rssService.get().batchMarkAsRead(articleIds = articleIdSet, isUnread = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun commitDiff() {
|
||||||
|
applicationScope.launch(ioDispatcher) {
|
||||||
|
val markAsReadArticles =
|
||||||
|
diffMap.filter { !it.value.isUnread }.map { it.key }.toSet()
|
||||||
|
val markAsUnreadArticles =
|
||||||
|
diffMap.filter { it.value.isUnread }.map { it.key }.toSet()
|
||||||
|
|
||||||
|
rssService.get()
|
||||||
|
.batchMarkAsRead(articleIds = markAsReadArticles, isUnread = false)
|
||||||
|
rssService.get()
|
||||||
|
.batchMarkAsRead(articleIds = markAsUnreadArticles, isUnread = true)
|
||||||
|
|
||||||
|
}.invokeOnCompletion { diffMap.clear() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class FlowUiState(
|
data class FlowUiState(
|
||||||
@ -105,3 +114,5 @@ data class FlowUiState(
|
|||||||
val isBack: Boolean = false,
|
val isBack: Boolean = false,
|
||||||
val syncWorkInfo: String = "",
|
val syncWorkInfo: String = "",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class Diff(val isUnread: Boolean)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user