Update feed ui

This commit is contained in:
Koitharu
2022-04-20 09:25:46 +03:00
parent ce8f57c3ca
commit f33dc8f797
11 changed files with 178 additions and 130 deletions

View File

@@ -0,0 +1,35 @@
package org.koitharu.kotatsu.base.ui.list.decor
import android.graphics.Rect
import android.util.SparseIntArray
import android.view.View
import androidx.core.util.getOrDefault
import androidx.core.util.set
import androidx.recyclerview.widget.RecyclerView
class TypedSpacingItemDecoration(
vararg spacingMapping: Pair<Int, Int>,
private val fallbackSpacing: Int = 0,
) : RecyclerView.ItemDecoration() {
private val mapping = SparseIntArray(spacingMapping.size)
init {
spacingMapping.forEach { (k, v) -> mapping[k] = v }
}
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
val itemType = parent.getChildViewHolder(view)?.itemViewType
val spacing = if (itemType == null) {
fallbackSpacing
} else {
mapping.getOrDefault(itemType, fallbackSpacing)
}
outRect.set(spacing, spacing, spacing, spacing)
}
}

View File

@@ -3,6 +3,9 @@ package org.koitharu.kotatsu.core.ui
import android.content.res.Resources
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.utils.ext.daysDiff
import org.koitharu.kotatsu.utils.ext.format
import java.util.*
sealed class DateTimeAgo : ListModel {
@@ -72,9 +75,33 @@ sealed class DateTimeAgo : ListModel {
override fun hashCode(): Int = days
}
class Absolute(private val date: Date) : DateTimeAgo() {
private val day = date.daysDiff(0)
override fun format(resources: Resources): String {
return date.format("d MMMM")
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Absolute
if (day != other.day) return false
return true
}
override fun hashCode(): Int {
return day
}
}
object LongAgo : DateTimeAgo() {
override fun format(resources: Resources): String {
return resources.getString(R.string.long_ago)
}
}
}
}

View File

@@ -11,7 +11,7 @@ import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.base.ui.list.PaginationScrollListener
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.base.ui.list.decor.TypedSpacingItemDecoration
import org.koitharu.kotatsu.databinding.FragmentFeedBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
@@ -57,7 +57,11 @@ class FeedFragment :
val spacing = resources.getDimensionPixelOffset(R.dimen.list_spacing)
paddingHorizontal = spacing
paddingVertical = resources.getDimensionPixelOffset(R.dimen.grid_spacing_outer)
addItemDecoration(SpacingItemDecoration(spacing))
val decoration = TypedSpacingItemDecoration(
FeedAdapter.ITEM_TYPE_FEED to 0,
fallbackSpacing = spacing
)
addItemDecoration(decoration)
}
viewModel.content.observe(viewLifecycleOwner, this::onListChanged)

View File

@@ -10,14 +10,15 @@ import kotlinx.coroutines.flow.filterNotNull
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.model.TrackingLogItem
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.core.ui.DateTimeAgo
import org.koitharu.kotatsu.list.ui.model.*
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.tracker.ui.model.toFeedItem
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.daysDiff
import java.util.*
import java.util.concurrent.TimeUnit
class FeedViewModel(
private val repository: TrackingRepository
@@ -34,8 +35,8 @@ class FeedViewModel(
hasNextPage
) { list, isHasNextPage ->
buildList(list.size + 2) {
add(header)
if (list.isEmpty()) {
add(header)
add(
EmptyState(
icon = R.drawable.ic_feed,
@@ -45,7 +46,7 @@ class FeedViewModel(
)
)
} else {
list.mapTo(this) { it.toFeedItem() }
list.mapListTo(this)
if (isHasNextPage) {
add(LoadingFooter)
}
@@ -85,4 +86,29 @@ class FeedViewModel(
onFeedCleared.postCall(Unit)
}
}
private fun List<TrackingLogItem>.mapListTo(destination: MutableList<ListModel>) {
var prevDate: DateTimeAgo? = null
for (item in this) {
val date = timeAgo(item.createdAt)
if (prevDate != date) {
destination += date
}
prevDate = date
destination += item.toFeedItem()
}
}
private fun timeAgo(date: Date): DateTimeAgo {
val diff = (System.currentTimeMillis() - date.time).coerceAtLeast(0L)
val diffMinutes = TimeUnit.MILLISECONDS.toMinutes(diff).toInt()
val diffDays = -date.daysDiff(System.currentTimeMillis())
return when {
diffMinutes < 3 -> DateTimeAgo.JustNow
diffDays < 1 -> DateTimeAgo.Today
diffDays == 1 -> DateTimeAgo.Yesterday
diffDays < 6 -> DateTimeAgo.DaysAgo(diffDays)
else -> DateTimeAgo.Absolute(date)
}
}
}

View File

@@ -4,10 +4,11 @@ import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import kotlin.jvm.internal.Intrinsics
import org.koitharu.kotatsu.core.ui.DateTimeAgo
import org.koitharu.kotatsu.list.ui.adapter.*
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.tracker.ui.model.FeedItem
import kotlin.jvm.internal.Intrinsics
class FeedAdapter(
coil: ImageLoader,
@@ -24,6 +25,7 @@ class FeedAdapter(
.addDelegate(ITEM_TYPE_ERROR_STATE, errorStateListAD(listener))
.addDelegate(ITEM_TYPE_EMPTY, emptyStateListAD(listener))
.addDelegate(ITEM_TYPE_HEADER, listHeaderAD())
.addDelegate(ITEM_TYPE_DATE_HEADER, relatedDateItemAD())
}
private class DiffCallback : DiffUtil.ItemCallback<ListModel>() {
@@ -32,6 +34,9 @@ class FeedAdapter(
oldItem is FeedItem && newItem is FeedItem -> {
oldItem.id == newItem.id
}
oldItem is DateTimeAgo && newItem is DateTimeAgo -> {
oldItem == newItem
}
else -> oldItem.javaClass == newItem.javaClass
}
@@ -49,5 +54,6 @@ class FeedAdapter(
const val ITEM_TYPE_ERROR_FOOTER = 4
const val ITEM_TYPE_EMPTY = 5
const val ITEM_TYPE_HEADER = 6
const val ITEM_TYPE_DATE_HEADER = 7
}
}

View File

@@ -12,7 +12,6 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.tracker.ui.model.FeedItem
import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.newImageRequest
import org.koitharu.kotatsu.utils.ext.textAndVisible
fun feedItemAD(
coil: ImageLoader,
@@ -38,13 +37,11 @@ fun feedItemAD(
.lifecycle(lifecycleOwner)
.enqueueWith(coil)
binding.textViewTitle.text = item.title
binding.badge.text = item.subtitle
binding.textViewChapters.text = item.chapters
binding.textViewTruncated.textAndVisible = if (item.truncated > 0) {
getString(R.string._and_x_more, item.truncated)
} else {
null
}
binding.textViewSummary.text = context.resources.getQuantityString(
R.plurals.new_chapters,
item.count,
item.count,
)
}
onViewRecycled {

View File

@@ -7,8 +7,6 @@ data class FeedItem(
val id: Long,
val imageUrl: String,
val title: String,
val subtitle: String,
val chapters: CharSequence,
val manga: Manga,
val truncated: Int,
val count: Int,
) : ListModel

View File

@@ -2,26 +2,10 @@ package org.koitharu.kotatsu.tracker.ui.model
import org.koitharu.kotatsu.core.model.TrackingLogItem
fun TrackingLogItem.toFeedItem(): FeedItem {
val truncate = chapters.size > MAX_CHAPTERS
val chaptersString = if (truncate) {
chapters.joinToString(
separator = "\n",
limit = MAX_CHAPTERS - 1,
truncated = "",
).trimEnd()
} else {
chapters.joinToString("\n")
}
return FeedItem(
id = id,
imageUrl = manga.coverUrl,
title = manga.title,
subtitle = chapters.size.toString(),
chapters = chaptersString,
manga = manga,
truncated = chapters.size - MAX_CHAPTERS + 1,
)
}
private const val MAX_CHAPTERS = 6
fun TrackingLogItem.toFeedItem() = FeedItem(
id = id,
imageUrl = manga.coverUrl,
title = manga.title,
count = chapters.size,
manga = manga,
)