Update recommendations item in explore section

This commit is contained in:
Koitharu
2024-04-08 19:26:42 +03:00
parent 018c84b6af
commit 7f5ff1ab14
10 changed files with 226 additions and 97 deletions

View File

@@ -0,0 +1,143 @@
package org.koitharu.kotatsu.core.ui.widgets
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
import androidx.core.content.withStyledAttributes
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver
import androidx.viewpager2.widget.ViewPager2
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.measureDimension
import org.koitharu.kotatsu.core.util.ext.resolveDp
import org.koitharu.kotatsu.parsers.util.toIntUp
class DotsIndicator @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : View(context, attrs, defStyleAttr) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private var indicatorSize = context.resources.resolveDp(12f)
private var dotSpacing = 0f
private var positionOffset: Float = 0f
var max: Int = 6
set(value) {
if (field != value) {
field = value
requestLayout()
invalidate()
}
}
var position: Int = 2
set(value) {
if (field != value) {
field = value
invalidate()
}
}
init {
paint.strokeWidth = context.resources.resolveDp(1.5f)
context.withStyledAttributes(attrs, R.styleable.DotsIndicator, defStyleAttr) {
paint.color = getColor(
R.styleable.DotsIndicator_dotColor,
context.getThemeColor(com.google.android.material.R.attr.colorPrimary, Color.DKGRAY),
)
indicatorSize = getDimension(R.styleable.DotsIndicator_dotSize, indicatorSize)
dotSpacing = getDimension(R.styleable.DotsIndicator_dotSpacing, dotSpacing)
}
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val dotSize = getDotSize()
val y = paddingTop + (height - paddingTop - paddingBottom) / 2f
var x = paddingLeft + dotSize / 2f
val radius = dotSize / 2f - paint.strokeWidth
val spacing = (width - paddingLeft - paddingRight) / max.toFloat() - dotSize
x += spacing / 2f
paint.style = Paint.Style.STROKE
for (i in 0 until max) {
canvas.drawCircle(x, y, radius, paint)
if (i == position) {
paint.style = Paint.Style.FILL
paint.alpha = (255 * (1f - positionOffset)).toInt()
canvas.drawCircle(x, y, radius, paint)
paint.alpha = 255
paint.style = Paint.Style.STROKE
}
if (i == position + 1 && positionOffset > 0f) {
paint.style = Paint.Style.FILL
paint.alpha = (255 * positionOffset).toInt()
canvas.drawCircle(x, y, radius, paint)
paint.alpha = 255
paint.style = Paint.Style.STROKE
}
x += spacing + dotSize
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val dotSize = getDotSize()
val desiredHeight = (dotSize + paddingTop + paddingBottom).toIntUp()
val desiredWidth = ((dotSize + dotSpacing) * max).toIntUp() + paddingLeft + paddingRight
setMeasuredDimension(
measureDimension(desiredWidth, widthMeasureSpec),
measureDimension(desiredHeight, heightMeasureSpec),
)
}
fun bindToViewPager(pager: ViewPager2) {
pager.registerOnPageChangeCallback(ViewPagerCallback())
pager.adapter?.let {
it.registerAdapterDataObserver(AdapterObserver(it))
}
}
private fun getDotSize() = if (indicatorSize <= 0) {
(height - paddingTop - paddingBottom).toFloat()
} else {
indicatorSize
}
private inner class ViewPagerCallback : ViewPager2.OnPageChangeCallback() {
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
super.onPageScrolled(position, positionOffset, positionOffsetPixels)
this@DotsIndicator.position = position
this@DotsIndicator.positionOffset = positionOffset
invalidate()
}
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
this@DotsIndicator.position = position
}
}
private inner class AdapterObserver(
private val adapter: RecyclerView.Adapter<*>,
) : AdapterDataObserver() {
override fun onChanged() {
super.onChanged()
max = adapter.itemCount
}
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
super.onItemRangeInserted(positionStart, itemCount)
max = adapter.itemCount
}
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
super.onItemRangeRemoved(positionStart, itemCount)
max = adapter.itemCount
}
}
}

View File

@@ -25,10 +25,12 @@ import org.koitharu.kotatsu.explore.domain.ExploreRepository
import org.koitharu.kotatsu.explore.ui.model.ExploreButtons
import org.koitharu.kotatsu.explore.ui.model.MangaSourceItem
import org.koitharu.kotatsu.explore.ui.model.RecommendationsItem
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
import org.koitharu.kotatsu.list.ui.model.EmptyHint
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.list.ui.model.MangaListModel
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
@@ -120,16 +122,16 @@ class ExploreViewModel @Inject constructor(
private fun buildList(
sources: List<MangaSource>,
recommendation: Manga?,
recommendation: List<Manga>,
isGrid: Boolean,
randomLoading: Boolean,
newSources: Set<MangaSource>,
): List<ListModel> {
val result = ArrayList<ListModel>(sources.size + 3)
result += ExploreButtons(randomLoading)
if (recommendation != null) {
result += ListHeader(R.string.suggestions)
result += RecommendationsItem(recommendation)
if (recommendation.isNotEmpty()) {
result += ListHeader(R.string.suggestions, R.string.more)
result += RecommendationsItem(recommendation.toRecommendationList())
}
if (sources.isNotEmpty()) {
result += ListHeader(
@@ -157,13 +159,25 @@ class ExploreViewModel @Inject constructor(
private fun getSuggestionFlow() = isSuggestionsEnabled.mapLatest { isEnabled ->
if (isEnabled) {
runCatchingCancellable {
suggestionRepository.getRandom()
}.getOrNull()
suggestionRepository.getRandomList(8)
}.getOrDefault(emptyList())
} else {
null
emptyList()
}
}
private fun List<Manga>.toRecommendationList() = map { manga ->
MangaListModel(
id = manga.id,
title = manga.title,
subtitle = manga.tags.joinToString { it.title },
coverUrl = manga.coverUrl,
manga = manga,
counter = 0,
progress = PROGRESS_NONE,
)
}
companion object {
private const val TIP_SUGGESTIONS = "suggestions"

View File

@@ -27,7 +27,7 @@ class ExploreAdapter(
addDelegate(ListItemType.EXPLORE_BUTTONS, exploreButtonsAD(listener))
addDelegate(
ListItemType.EXPLORE_SUGGESTION,
exploreRecommendationItemAD(coil, listener, mangaClickListener, lifecycleOwner),
exploreRecommendationItemAD(coil, mangaClickListener, lifecycleOwner),
)
addDelegate(ListItemType.HEADER, listHeaderAD(listener))
addDelegate(ListItemType.EXPLORE_SOURCE_LIST, exploreSourceListItemAD(coil, clickListener, lifecycleOwner))

View File

@@ -8,6 +8,7 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.getSummary
import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
@@ -22,10 +23,13 @@ import org.koitharu.kotatsu.databinding.ItemExploreButtonsBinding
import org.koitharu.kotatsu.databinding.ItemExploreSourceGridBinding
import org.koitharu.kotatsu.databinding.ItemExploreSourceListBinding
import org.koitharu.kotatsu.databinding.ItemRecommendationBinding
import org.koitharu.kotatsu.databinding.ItemRecommendationMangaBinding
import org.koitharu.kotatsu.explore.ui.model.ExploreButtons
import org.koitharu.kotatsu.explore.ui.model.MangaSourceItem
import org.koitharu.kotatsu.explore.ui.model.RecommendationsItem
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.MangaListModel
import org.koitharu.kotatsu.parsers.model.Manga
fun exploreButtonsAD(
@@ -51,21 +55,37 @@ fun exploreButtonsAD(
fun exploreRecommendationItemAD(
coil: ImageLoader,
clickListener: View.OnClickListener,
itemClickListener: OnListItemClickListener<Manga>,
lifecycleOwner: LifecycleOwner,
) = adapterDelegateViewBinding<RecommendationsItem, ListModel, ItemRecommendationBinding>(
{ layoutInflater, parent -> ItemRecommendationBinding.inflate(layoutInflater, parent, false) },
) {
binding.buttonMore.setOnClickListener(clickListener)
val adapter = BaseListAdapter<MangaListModel>()
.addDelegate(ListItemType.MANGA_LIST, recommendationMangaItemAD(coil, itemClickListener, lifecycleOwner))
binding.pager.adapter = adapter
binding.dots.bindToViewPager(binding.pager)
bind {
adapter.items = item.manga
}
}
fun recommendationMangaItemAD(
coil: ImageLoader,
itemClickListener: OnListItemClickListener<Manga>,
lifecycleOwner: LifecycleOwner,
) = adapterDelegateViewBinding<MangaListModel, MangaListModel, ItemRecommendationMangaBinding>(
{ layoutInflater, parent -> ItemRecommendationMangaBinding.inflate(layoutInflater, parent, false) },
) {
binding.root.setOnClickListener { v ->
itemClickListener.onItemClick(item.manga, v)
}
bind {
binding.textViewTitle.text = item.manga.title
binding.textViewSubtitle.textAndVisible = item.summary
binding.textViewSubtitle.textAndVisible = item.subtitle
binding.imageViewCover.newImageRequest(lifecycleOwner, item.manga.coverUrl)?.run {
placeholder(R.drawable.ic_placeholder)
fallback(R.drawable.ic_placeholder)
@@ -78,6 +98,7 @@ fun exploreRecommendationItemAD(
}
}
fun exploreSourceListItemAD(
coil: ImageLoader,
listener: OnListItemClickListener<MangaSourceItem>,

View File

@@ -1,12 +1,11 @@
package org.koitharu.kotatsu.explore.ui.model
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.list.ui.model.MangaListModel
data class RecommendationsItem(
val manga: Manga
val manga: List<MangaListModel>
) : ListModel {
val summary: String = manga.tags.joinToString { it.title }
override fun areItemsTheSame(other: ListModel): Boolean {
return other is RecommendationsItem

View File

@@ -34,6 +34,10 @@ class SuggestionRepository @Inject constructor(
}
}
suspend fun getRandomList(limit: Int): List<Manga> {
return List(limit) { getRandom() }.filterNotNull().distinct() //TODO improve
}
suspend fun clear() {
db.getSuggestionDao().deleteAll()
}

View File

@@ -1,70 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
<LinearLayout
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:orientation="vertical">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/imageView_cover"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="8dp"
android:layout_marginTop="16dp"
android:scaleType="centerCrop"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
tools:src="@tools:sample/backgrounds/scenic" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="@dimen/recommendation_item_height" />
<TextView
android:id="@+id/textView_title"
android:layout_width="0dp"
<org.koitharu.kotatsu.core.ui.widgets.DotsIndicator
android:id="@+id/dots"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="?attr/textAppearanceBodyLarge"
app:layout_constraintBottom_toTopOf="@+id/textView_subtitle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/imageView_cover"
app:layout_constraintTop_toTopOf="@+id/imageView_cover"
tools:text="@tools:sample/lorem" />
android:layout_gravity="center_horizontal"
android:layout_marginVertical="@dimen/margin_small"
app:dotSize="8dp"
app:dotSpacing="6dp" />
<TextView
android:id="@+id/textView_subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="?attr/textAppearanceBodyMedium"
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="@tools:sample/lorem/random" />
<Button
android:id="@+id/button_more"
style="@style/Widget.Kotatsu.ExploreButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:gravity="center"
android:text="@string/more"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textView_subtitle" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>

View File

@@ -4,9 +4,9 @@
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:layout_height="match_parent"
android:background="@drawable/list_selector"
android:clipChildren="false">
tools:layout_height="@dimen/recommendation_item_height">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/imageView_cover"
@@ -27,10 +27,9 @@
android:layout_marginStart="16dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:singleLine="true"
android:textAppearance="?attr/textAppearanceBodyLarge"
app:layout_constraintBottom_toTopOf="@+id/textView_subtitle"
app:layout_constraintEnd_toStartOf="@id/button_more"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/imageView_cover"
app:layout_constraintTop_toTopOf="@+id/imageView_cover"
tools:text="@tools:sample/lorem" />
@@ -42,26 +41,12 @@
android:layout_marginStart="16dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="?attr/textAppearanceBodyMedium"
app:layout_constraintBottom_toBottomOf="@+id/imageView_cover"
app:layout_constraintEnd_toStartOf="@id/button_more"
app:layout_constraintStart_toEndOf="@+id/imageView_cover"
app:layout_constraintTop_toBottomOf="@+id/textView_title"
tools:text="@tools:sample/lorem/random" />
<Button
android:id="@+id/button_more"
style="@style/Widget.Kotatsu.ExploreButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:gravity="center"
android:minWidth="120dp"
android:text="@string/more"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintStart_toEndOf="@+id/imageView_cover"
app:layout_constraintTop_toBottomOf="@+id/textView_title"
app:layout_constraintVertical_bias="0"
tools:text="@tools:sample/lorem/random" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -159,4 +159,12 @@
<attr name="readOnly" format="boolean" />
</declare-styleable>
<declare-styleable name="DotsIndicator">
<attr name="dotSize" format="dimension">
<enum name="fit" value="-1" />
</attr>
<attr name="dotColor" format="color" />
<attr name="dotSpacing" format="dimension" />
</declare-styleable>
</resources>

View File

@@ -20,6 +20,7 @@
<dimen name="grid_spacing_outer_double">4dp</dimen>
<dimen name="manga_list_item_height">86dp</dimen>
<dimen name="manga_list_details_item_height">120dp</dimen>
<dimen name="recommendation_item_height">90dp</dimen>
<dimen name="bookmark_item_height">120dp</dimen>
<dimen name="bookmark_list_spacing">4dp</dimen>
<dimen name="chapter_list_item_height">48dp</dimen>