Update feed ui

pull/153/head
Koitharu 4 years ago
parent ce8f57c3ca
commit f33dc8f797
No known key found for this signature in database
GPG Key ID: 8E861F8CE6E7CE27

@ -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 android.content.res.Resources
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.list.ui.model.ListModel 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 { sealed class DateTimeAgo : ListModel {
@ -72,6 +75,30 @@ sealed class DateTimeAgo : ListModel {
override fun hashCode(): Int = days 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() { object LongAgo : DateTimeAgo() {
override fun format(resources: Resources): String { override fun format(resources: Resources): String {
return resources.getString(R.string.long_ago) 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.R
import org.koitharu.kotatsu.base.ui.BaseFragment import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.base.ui.list.PaginationScrollListener 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.databinding.FragmentFeedBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
@ -57,7 +57,11 @@ class FeedFragment :
val spacing = resources.getDimensionPixelOffset(R.dimen.list_spacing) val spacing = resources.getDimensionPixelOffset(R.dimen.list_spacing)
paddingHorizontal = spacing paddingHorizontal = spacing
paddingVertical = resources.getDimensionPixelOffset(R.dimen.grid_spacing_outer) 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) viewModel.content.observe(viewLifecycleOwner, this::onListChanged)

@ -10,14 +10,15 @@ import kotlinx.coroutines.flow.filterNotNull
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.model.TrackingLogItem import org.koitharu.kotatsu.core.model.TrackingLogItem
import org.koitharu.kotatsu.list.ui.model.EmptyState import org.koitharu.kotatsu.core.ui.DateTimeAgo
import org.koitharu.kotatsu.list.ui.model.ListHeader import org.koitharu.kotatsu.list.ui.model.*
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.tracker.ui.model.toFeedItem import org.koitharu.kotatsu.tracker.ui.model.toFeedItem
import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.daysDiff
import java.util.*
import java.util.concurrent.TimeUnit
class FeedViewModel( class FeedViewModel(
private val repository: TrackingRepository private val repository: TrackingRepository
@ -34,8 +35,8 @@ class FeedViewModel(
hasNextPage hasNextPage
) { list, isHasNextPage -> ) { list, isHasNextPage ->
buildList(list.size + 2) { buildList(list.size + 2) {
add(header)
if (list.isEmpty()) { if (list.isEmpty()) {
add(header)
add( add(
EmptyState( EmptyState(
icon = R.drawable.ic_feed, icon = R.drawable.ic_feed,
@ -45,7 +46,7 @@ class FeedViewModel(
) )
) )
} else { } else {
list.mapTo(this) { it.toFeedItem() } list.mapListTo(this)
if (isHasNextPage) { if (isHasNextPage) {
add(LoadingFooter) add(LoadingFooter)
} }
@ -85,4 +86,29 @@ class FeedViewModel(
onFeedCleared.postCall(Unit) 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 androidx.recyclerview.widget.DiffUtil
import coil.ImageLoader import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter 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.adapter.*
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.tracker.ui.model.FeedItem import org.koitharu.kotatsu.tracker.ui.model.FeedItem
import kotlin.jvm.internal.Intrinsics
class FeedAdapter( class FeedAdapter(
coil: ImageLoader, coil: ImageLoader,
@ -24,6 +25,7 @@ class FeedAdapter(
.addDelegate(ITEM_TYPE_ERROR_STATE, errorStateListAD(listener)) .addDelegate(ITEM_TYPE_ERROR_STATE, errorStateListAD(listener))
.addDelegate(ITEM_TYPE_EMPTY, emptyStateListAD(listener)) .addDelegate(ITEM_TYPE_EMPTY, emptyStateListAD(listener))
.addDelegate(ITEM_TYPE_HEADER, listHeaderAD()) .addDelegate(ITEM_TYPE_HEADER, listHeaderAD())
.addDelegate(ITEM_TYPE_DATE_HEADER, relatedDateItemAD())
} }
private class DiffCallback : DiffUtil.ItemCallback<ListModel>() { private class DiffCallback : DiffUtil.ItemCallback<ListModel>() {
@ -32,6 +34,9 @@ class FeedAdapter(
oldItem is FeedItem && newItem is FeedItem -> { oldItem is FeedItem && newItem is FeedItem -> {
oldItem.id == newItem.id oldItem.id == newItem.id
} }
oldItem is DateTimeAgo && newItem is DateTimeAgo -> {
oldItem == newItem
}
else -> oldItem.javaClass == newItem.javaClass else -> oldItem.javaClass == newItem.javaClass
} }
@ -49,5 +54,6 @@ class FeedAdapter(
const val ITEM_TYPE_ERROR_FOOTER = 4 const val ITEM_TYPE_ERROR_FOOTER = 4
const val ITEM_TYPE_EMPTY = 5 const val ITEM_TYPE_EMPTY = 5
const val ITEM_TYPE_HEADER = 6 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.tracker.ui.model.FeedItem
import org.koitharu.kotatsu.utils.ext.enqueueWith import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.newImageRequest import org.koitharu.kotatsu.utils.ext.newImageRequest
import org.koitharu.kotatsu.utils.ext.textAndVisible
fun feedItemAD( fun feedItemAD(
coil: ImageLoader, coil: ImageLoader,
@ -38,13 +37,11 @@ fun feedItemAD(
.lifecycle(lifecycleOwner) .lifecycle(lifecycleOwner)
.enqueueWith(coil) .enqueueWith(coil)
binding.textViewTitle.text = item.title binding.textViewTitle.text = item.title
binding.badge.text = item.subtitle binding.textViewSummary.text = context.resources.getQuantityString(
binding.textViewChapters.text = item.chapters R.plurals.new_chapters,
binding.textViewTruncated.textAndVisible = if (item.truncated > 0) { item.count,
getString(R.string._and_x_more, item.truncated) item.count,
} else { )
null
}
} }
onViewRecycled { onViewRecycled {

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

@ -2,26 +2,10 @@ package org.koitharu.kotatsu.tracker.ui.model
import org.koitharu.kotatsu.core.model.TrackingLogItem import org.koitharu.kotatsu.core.model.TrackingLogItem
fun TrackingLogItem.toFeedItem(): FeedItem { 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, id = id,
imageUrl = manga.coverUrl, imageUrl = manga.coverUrl,
title = manga.title, title = manga.title,
subtitle = chapters.size.toString(), count = chapters.size,
chapters = chaptersString,
manga = manga, manga = manga,
truncated = chapters.size - MAX_CHAPTERS + 1,
) )
}
private const val MAX_CHAPTERS = 6

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

@ -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>
Loading…
Cancel
Save