Adjust feed list
This commit is contained in:
@@ -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 -> {
|
||||||
|
|||||||
Reference in New Issue
Block a user