Adjust feed list

master
Zakhar Timoshenko 2 years ago
parent d947e8234d
commit f0f25a4d48
Signed by: Xtimms
SSH Key Fingerprint: SHA256:wH6spYepK/A5erBh7ZyAnr1ru9H4eaMVBEuiw6DSpxI

@ -22,7 +22,7 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListUpdateCallback import androidx.recyclerview.widget.ListUpdateCallback
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.xtimms.tokusho.sections.history.HistoryItemModel import org.xtimms.tokusho.core.model.ListModel
import java.time.Instant import java.time.Instant
enum class RowEntityType { Header, Item } enum class RowEntityType { Header, Item }
@ -32,7 +32,7 @@ data class RowEntity(
val key: String, val key: String,
var contentHash: String? = null, var contentHash: String? = null,
val day: Instant, val day: Instant,
var historyItemModel: HistoryItemModel?, var itemModel: ListModel?,
) )
@SuppressLint("ComposableNaming", "UnusedTransitionTargetStateParameter") @SuppressLint("ComposableNaming", "UnusedTransitionTargetStateParameter")
@ -126,7 +126,7 @@ fun updateAnimatedItemsState(
override fun onChanged(position: Int, count: Int, payload: Any?) { override fun onChanged(position: Int, count: Int, payload: Any?) {
for (i in 0 until count) { for (i in 0 until count) {
compositeList[position + i].item.historyItemModel = (payload as RowEntity).historyItemModel compositeList[position + i].item.itemModel = (payload as RowEntity).itemModel
compositeList[position + i].item.contentHash = payload.contentHash compositeList[position + i].item.contentHash = payload.contentHash
} }
} }

@ -1,6 +1,7 @@
package org.xtimms.tokusho.core.tracker.model package org.xtimms.tokusho.core.tracker.model
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.xtimms.tokusho.core.model.ListModel
import java.time.Instant import java.time.Instant
data class TrackingLogItem( data class TrackingLogItem(
@ -9,4 +10,10 @@ data class TrackingLogItem(
val chapters: List<String>, val chapters: List<String>,
val createdAt: Instant, val createdAt: Instant,
val isNew: Boolean, val isNew: Boolean,
) ) : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean {
return other is TrackingLogItem && other.manga.id == manga.id
}
}

@ -7,8 +7,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ClearAll import androidx.compose.material.icons.outlined.ClearAll
import androidx.compose.material.icons.outlined.Refresh import androidx.compose.material.icons.outlined.Refresh
@ -39,10 +37,15 @@ import org.xtimms.tokusho.core.components.DismissButton
import org.xtimms.tokusho.core.components.ListGroupHeader import org.xtimms.tokusho.core.components.ListGroupHeader
import org.xtimms.tokusho.core.components.ScaffoldWithClassicTopAppBar import org.xtimms.tokusho.core.components.ScaffoldWithClassicTopAppBar
import org.xtimms.tokusho.core.components.TokushoDialog import org.xtimms.tokusho.core.components.TokushoDialog
import org.xtimms.tokusho.core.components.effects.RowEntity
import org.xtimms.tokusho.core.components.effects.RowEntityType
import org.xtimms.tokusho.core.components.effects.animatedItemsIndexed
import org.xtimms.tokusho.core.components.effects.updateAnimatedItemsState
import org.xtimms.tokusho.core.screens.EmptyScreen import org.xtimms.tokusho.core.screens.EmptyScreen
import org.xtimms.tokusho.core.tracker.model.TrackingLogItem import org.xtimms.tokusho.core.tracker.model.TrackingLogItem
import org.xtimms.tokusho.sections.feed.model.toFeedItem import org.xtimms.tokusho.sections.feed.model.toFeedItem
import org.xtimms.tokusho.utils.lang.calculateTimeAgo import org.xtimms.tokusho.utils.lang.calculateTimeAgo
import org.xtimms.tokusho.utils.lang.isSameDay
import java.time.Instant import java.time.Instant
const val FEED_DESTINATION = "feed" const val FEED_DESTINATION = "feed"
@ -60,6 +63,39 @@ fun FeedView(
val feed by viewModel.content.collectAsStateWithLifecycle(emptyList()) val feed by viewModel.content.collectAsStateWithLifecycle(emptyList())
val animatedList = run {
val list = emptyList<RowEntity>().toMutableList()
var createdAt: Instant? = null
feed.forEach { item ->
if (createdAt === null || !isSameDay(
item.createdAt.toEpochMilli(),
createdAt!!.toEpochMilli()
)
) {
createdAt = item.createdAt
list.add(
RowEntity(
type = RowEntityType.Header,
key = "header-${createdAt}",
itemModel = null,
day = createdAt!!,
)
)
}
list.add(
RowEntity(
type = RowEntityType.Item,
key = "item-${item.manga.id}-${item.createdAt}",
day = createdAt!!,
itemModel = item
)
)
}
updateAnimatedItemsState(newList = list.toList().map { it })
}
ScaffoldWithClassicTopAppBar( ScaffoldWithClassicTopAppBar(
title = stringResource(R.string.feed), title = stringResource(R.string.feed),
navigateBack = navigateBack, navigateBack = navigateBack,
@ -93,7 +129,29 @@ fun FeedView(
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = padding contentPadding = padding
) { ) {
feedUiItems(coil, viewModel.getUiModel())
animatedItemsIndexed(
state = animatedList.value,
key = { rowItem -> rowItem.key },
) { _, item ->
when (item.type) {
RowEntityType.Header -> ListGroupHeader(
calculateTimeAgo(item.day).format(
LocalContext.current.resources
)
)
RowEntityType.Item -> FeedViewItem(
modifier = Modifier.animateItemPlacement(),
coil = coil,
selected = false,
feed = (item.itemModel as TrackingLogItem).toFeedItem(),
onClick = { /*TODO*/ },
onLongClick = { /*TODO*/ }
)
}
}
} }
} }
if (feed.isEmpty()) { if (feed.isEmpty()) {
@ -180,51 +238,3 @@ private fun ClearFeedDialogPreview() {
isClearInfoAboutNewChaptersSelected = false isClearInfoAboutNewChaptersSelected = false
) )
} }
sealed interface FeedUiModel {
data class Header(val date: Instant) : FeedUiModel
data class Item(val item: TrackingLogItem) : FeedUiModel
}
@OptIn(ExperimentalFoundationApi::class)
internal fun LazyListScope.feedUiItems(
coil: ImageLoader,
uiModels: List<FeedUiModel>
) {
items(
items = uiModels,
contentType = {
when (it) {
is FeedUiModel.Header -> "header"
is FeedUiModel.Item -> "item"
}
},
key = {
when (it) {
is FeedUiModel.Header -> "feedHeader-${it.hashCode()}"
is FeedUiModel.Item -> "feed-${it.item.manga.id}"
}
},
) { item ->
when (item) {
is FeedUiModel.Header -> {
ListGroupHeader(
modifier = Modifier.animateItemPlacement(),
text = calculateTimeAgo(item.date).format(
LocalContext.current.resources
)
)
}
is FeedUiModel.Item -> {
val track = item.item
FeedViewItem(
modifier = Modifier.animateItemPlacement(),
coil = coil,
selected = false,
feed = track.toFeedItem(),
onClick = { /*TODO*/ },
onLongClick = { /*TODO*/ })
}
}
}
}

@ -46,7 +46,7 @@ fun FeedViewItem(
) { ) {
val haptic = LocalHapticFeedback.current val haptic = LocalHapticFeedback.current
val textAlpha = if (!feed.isNew) 1f else ReadItemAlpha val textAlpha = if (feed.isNew) 1f else ReadItemAlpha
Row( Row(
modifier = modifier modifier = modifier
@ -85,7 +85,7 @@ fun FeedViewItem(
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
var textHeight by remember { mutableIntStateOf(0) } var textHeight by remember { mutableIntStateOf(0) }
if (!feed.isNew) { if (feed.isNew) {
Icon( Icon(
imageVector = Icons.Filled.Circle, imageVector = Icons.Filled.Circle,
contentDescription = stringResource(R.string.unread), contentDescription = stringResource(R.string.unread),

@ -54,18 +54,4 @@ class FeedViewModel @Inject constructor(
fun updateFeed() { fun updateFeed() {
trackScheduler.startNow() trackScheduler.startNow()
} }
fun getUiModel(): List<FeedUiModel> {
return content.value
.map { FeedUiModel.Item(it) }
.insertSeparators { before, after ->
val beforeDate = before?.item?.createdAt
val afterDate = after?.item?.createdAt
when {
beforeDate != afterDate && afterDate != null -> FeedUiModel.Header(afterDate)
// Return null to avoid adding a separator between two items.
else -> null
}
}
}
} }

@ -31,6 +31,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -97,7 +98,7 @@ fun HistoryView(
RowEntity( RowEntity(
type = RowEntityType.Header, type = RowEntityType.Header,
key = "header-${readDate}", key = "header-${readDate}",
historyItemModel = null, itemModel = null,
day = readDate!!, day = readDate!!,
) )
) )
@ -107,7 +108,7 @@ fun HistoryView(
type = RowEntityType.Item, type = RowEntityType.Item,
key = "item-${item.manga.id}", key = "item-${item.manga.id}",
day = readDate!!, day = readDate!!,
historyItemModel = item itemModel = item
) )
) )
} }
@ -115,7 +116,9 @@ fun HistoryView(
} }
Box( Box(
Modifier.fillMaxSize() modifier = Modifier
.clipToBounds()
.fillMaxSize(),
) { ) {
history.let { history.let {
if (it == null) { if (it == null) {
@ -140,7 +143,7 @@ fun HistoryView(
animatedItemsIndexed( animatedItemsIndexed(
state = animatedList.value, state = animatedList.value,
key = { rowItem -> rowItem.key }, key = { rowItem -> rowItem.key },
) { index, item -> ) { _, item ->
when (item.type) { when (item.type) {
RowEntityType.Header -> ListGroupHeader( RowEntityType.Header -> ListGroupHeader(
calculateTimeAgo(item.day).format( calculateTimeAgo(item.day).format(
@ -157,7 +160,7 @@ fun HistoryView(
icon = Icons.Outlined.DeleteForever, icon = Icons.Outlined.DeleteForever,
stayDismissed = true, stayDismissed = true,
onDismiss = { onDismiss = {
viewModel.removeFromHistory(item.historyItemModel!!) viewModel.removeFromHistory(item.itemModel!! as HistoryItemModel)
} }
), ),
endActionsConfig = SwipeActionsConfig( endActionsConfig = SwipeActionsConfig(
@ -233,8 +236,8 @@ fun HistoryView(
) { ) {
HistoryItem( HistoryItem(
coil = coil, coil = coil,
history = item.historyItemModel!!, history = (item.itemModel!! as HistoryItemModel),
onClick = { navigateToDetails(item.historyItemModel!!.manga.id) }, onClick = { navigateToDetails((item.itemModel!! as HistoryItemModel).manga.id) },
) )
} }
} }

@ -29,10 +29,7 @@ fun calculateTimeAgo(instant: Instant, showMonths: Boolean = false): DateTimeAgo
val diffDays = localDate.until(now, ChronoUnit.DAYS) val diffDays = localDate.until(now, ChronoUnit.DAYS)
return when { return when {
diffDays == 0L -> { diffDays == 0L -> DateTimeAgo.Today
if (instant.until(Instant.now(), ChronoUnit.MINUTES) < 3) DateTimeAgo.JustNow
else DateTimeAgo.Today
}
diffDays == 1L -> DateTimeAgo.Yesterday diffDays == 1L -> DateTimeAgo.Yesterday
diffDays < 6 -> DateTimeAgo.DaysAgo(diffDays.toInt()) diffDays < 6 -> DateTimeAgo.DaysAgo(diffDays.toInt())
else -> { else -> {

Loading…
Cancel
Save