Update feed ui
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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" />
|
||||
56
app/src/main/res/layout/item_feed.xml
Normal file
56
app/src/main/res/layout/item_feed.xml
Normal 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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user