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,
)

View File

@@ -14,4 +14,4 @@
android:paddingBottom="@dimen/grid_spacing_outer"
app:fastScrollEnabled="true"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_tracklog" />
tools:listitem="@layout/item_feed" />

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/list_selector"
android:clipChildren="false"
android:padding="@dimen/list_spacing">
<org.koitharu.kotatsu.base.ui.widgets.CoverImageView
android:id="@+id/imageView_cover"
android:layout_width="48dp"
android:layout_height="0dp"
android:contentDescription="@null"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="h,1:1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
tools:src="@tools:sample/backgrounds/scenic" />
<TextView
android:id="@+id/textView_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="2dp"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="?attr/textAppearanceBodyLarge"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/imageView_cover"
app:layout_constraintTop_toTopOf="@id/imageView_cover"
tools:text="@tools:sample/lorem" />
<TextView
android:id="@+id/textView_summary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="2dp"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="?attr/textAppearanceBodySmall"
app:layout_constraintBottom_toBottomOf="@id/imageView_cover"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/imageView_cover"
app:layout_constraintTop_toBottomOf="@id/textView_title"
tools:text="@string/new_chapters" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,85 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="12dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<org.koitharu.kotatsu.base.ui.widgets.CoverImageView
android:id="@+id/imageView_cover"
android:layout_width="wrap_content"
android:layout_height="@dimen/manga_list_details_item_height"
android:orientation="vertical"
android:scaleType="centerCrop"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover"
tools:src="@tools:sample/backgrounds/scenic" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/textView_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginEnd="4dp"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="2"
android:textAppearance="?attr/textAppearanceTitleMedium"
tools:text="@tools:sample/lorem/random" />
<TextView
android:id="@+id/badge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
android:background="@drawable/badge"
android:paddingHorizontal="6dp"
android:paddingVertical="2dp"
android:textColor="?attr/colorOnTertiary"
android:textSize="12sp"
android:textStyle="bold"
tools:text="54" />
</LinearLayout>
<TextView
android:id="@+id/textView_chapters"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:ellipsize="none"
android:lineSpacingExtra="4sp"
android:textAppearance="?attr/textAppearanceBodySmall"
tools:text="@tools:sample/lorem[10]" />
<TextView
android:id="@+id/textView_truncated"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textColor="?android:textColorHint"
tools:text="@string/_and_x_more" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>