Chapters list grouping

This commit is contained in:
Koitharu
2024-04-21 10:01:57 +03:00
parent 1fe5095654
commit fb716d300e
14 changed files with 331 additions and 69 deletions

View File

@@ -72,7 +72,7 @@ abstract class BaseActivity<B : ViewBinding> :
onBackPressedDispatcher.addCallback(actionModeDelegate)
}
override fun onNewIntent(intent: Intent?) {
override fun onNewIntent(intent: Intent) {
putDataToExtras(intent)
super.onNewIntent(intent)
}

View File

@@ -0,0 +1,162 @@
package org.koitharu.kotatsu.core.ui.image
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.ColorFilter
import android.graphics.Outline
import android.graphics.Paint
import android.graphics.Path
import android.graphics.PixelFormat
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
import android.os.Build
import androidx.annotation.ReturnThis
import org.koitharu.kotatsu.core.util.ext.getThemeColorStateList
import org.koitharu.kotatsu.core.util.ext.resolveDp
import org.koitharu.kotatsu.parsers.util.toIntUp
import com.google.android.material.R as materialR
class CardDrawable(
context: Context,
private var corners: Int,
) : Drawable() {
private val cornerSize = context.resources.resolveDp(12f)
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val cornersF = FloatArray(8)
private val boundsF = RectF()
private val color: ColorStateList
private val path = Path()
private var alpha = 255
private var state: IntArray? = null
private var horizontalInset: Int = 0
init {
paint.style = Paint.Style.FILL
color = context.getThemeColorStateList(materialR.attr.colorSurfaceContainerHighest)
?: ColorStateList.valueOf(Color.TRANSPARENT)
setCorners(corners)
updateColor()
}
override fun draw(canvas: Canvas) {
canvas.drawPath(path, paint)
}
override fun setAlpha(alpha: Int) {
this.alpha = alpha
updateColor()
}
override fun setColorFilter(colorFilter: ColorFilter?) {
paint.colorFilter = colorFilter
}
override fun getColorFilter(): ColorFilter? = paint.colorFilter
override fun getOutline(outline: Outline) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
outline.setPath(path)
} else if (path.isConvex) {
outline.setConvexPath(path)
}
outline.alpha = 1f
}
override fun getPadding(padding: Rect): Boolean {
padding.set(
horizontalInset,
0,
horizontalInset,
0,
)
if (corners or TOP != 0) {
padding.top += cornerSize.toIntUp()
}
if (corners or BOTTOM != 0) {
padding.bottom += cornerSize.toIntUp()
}
return horizontalInset != 0
}
override fun onStateChange(state: IntArray): Boolean {
this.state = state
if (color.isStateful) {
updateColor()
return true
} else {
return false
}
}
@Deprecated("Deprecated in Java")
override fun getOpacity(): Int = PixelFormat.TRANSPARENT
override fun onBoundsChange(bounds: Rect) {
super.onBoundsChange(bounds)
boundsF.set(bounds)
boundsF.inset(horizontalInset.toFloat(), 0f)
path.reset()
path.addRoundRect(boundsF, cornersF, Path.Direction.CW)
path.close()
}
@ReturnThis
fun setCorners(corners: Int): CardDrawable {
this.corners = corners
val topLeft = if (corners and TOP_LEFT == TOP_LEFT) cornerSize else 0f
val topRight = if (corners and TOP_RIGHT == TOP_RIGHT) cornerSize else 0f
val bottomRight = if (corners and BOTTOM_RIGHT == BOTTOM_RIGHT) cornerSize else 0f
val bottomLeft = if (corners and BOTTOM_LEFT == BOTTOM_LEFT) cornerSize else 0f
cornersF[0] = topLeft
cornersF[1] = topLeft
cornersF[2] = topRight
cornersF[3] = topRight
cornersF[4] = bottomRight
cornersF[5] = bottomRight
cornersF[6] = bottomLeft
cornersF[7] = bottomLeft
invalidateSelf()
return this
}
fun setHorizontalInset(inset: Int) {
horizontalInset = inset
invalidateSelf()
}
private fun updateColor() {
paint.color = color.getColorForState(state, color.defaultColor)
paint.alpha = alpha
}
companion object {
const val TOP_LEFT = 1
const val TOP_RIGHT = 2
const val BOTTOM_LEFT = 4
const val BOTTOM_RIGHT = 8
const val LEFT = TOP_LEFT or BOTTOM_LEFT
const val TOP = TOP_LEFT or TOP_RIGHT
const val RIGHT = TOP_RIGHT or BOTTOM_RIGHT
const val BOTTOM = BOTTOM_LEFT or BOTTOM_RIGHT
const val NONE = 0
const val ALL = TOP_LEFT or TOP_RIGHT or BOTTOM_RIGHT or BOTTOM_LEFT
fun from(d: Drawable?): CardDrawable? = when (d) {
null -> null
is CardDrawable -> d
is LayerDrawable -> (0 until d.numberOfLayers).firstNotNullOfOrNull { i ->
from(d.getDrawable(i))
}
else -> null
}
}
}

View File

@@ -72,7 +72,8 @@ fun MangaDetails.mapChapters(
fun List<ChapterListItem>.withVolumeHeaders(context: Context): List<ListModel> {
var prevVolume = 0
val result = ArrayList<ListModel>((size * 1.4).toInt())
for (item in this) {
var groupPos: Byte = 0
for ((index, item) in this.withIndex()) {
val chapter = item.chapter
if (chapter.volume != prevVolume) {
val text = if (chapter.volume == 0) {
@@ -82,8 +83,19 @@ fun List<ChapterListItem>.withVolumeHeaders(context: Context): List<ListModel> {
}
result.add(ListHeader(text))
prevVolume = chapter.volume
groupPos = ChapterListItem.GROUP_START
} else if (groupPos == ChapterListItem.GROUP_START) {
groupPos = ChapterListItem.GROUP_MIDDLE
}
if (groupPos != 0.toByte()) {
val next = this.getOrNull(index + 1)
if (next == null || next.chapter.volume != prevVolume) {
groupPos = ChapterListItem.GROUP_END
}
result.add(item.copy(groupPosition = groupPos))
} else {
result.add(item)
}
result.add(item)
}
return result
}

View File

@@ -13,13 +13,13 @@ import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ItemChapterBinding
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.list.ui.model.ListModel
import com.google.android.material.R as MR
import com.google.android.material.R as materialR
fun chapterListItemAD(
clickListener: OnListItemClickListener<ChapterListItem>,
) = adapterDelegateViewBinding<ChapterListItem, ListModel, ItemChapterBinding>(
viewBinding = { inflater, parent -> ItemChapterBinding.inflate(inflater, parent, false) },
on = { item, _, _ -> item is ChapterListItem && !item.isGrid }
on = { item, _, _ -> item is ChapterListItem && !item.isGrid },
) {
val eventListener = AdapterDelegateClickListenerAdapter(this, clickListener)
@@ -27,10 +27,17 @@ fun chapterListItemAD(
itemView.setOnLongClickListener(eventListener)
bind { payloads ->
if (payloads.isEmpty()) {
binding.textViewTitle.text = item.chapter.name
binding.textViewDescription.textAndVisible = item.description
}
binding.textViewTitle.text = item.chapter.name
binding.textViewDescription.textAndVisible = item.description
itemView.setBackgroundResource(
when {
item.isGroupStart && item.isGroupEnd -> R.drawable.bg_card_full
item.isGroupStart -> R.drawable.bg_card_top
item.isGroupMiddle -> R.drawable.bg_card_none
item.isGroupEnd -> R.drawable.bg_card_bottom
else -> R.drawable.list_selector
},
)
when {
item.isCurrent -> {
binding.textViewTitle.drawableStart = ContextCompat.getDrawable(context, R.drawable.ic_current_chapter)
@@ -47,7 +54,7 @@ fun chapterListItemAD(
null
}
binding.textViewTitle.setTextColor(context.getThemeColorStateList(android.R.attr.textColorPrimary))
binding.textViewDescription.setTextColor(context.getThemeColorStateList(MR.attr.colorOutline))
binding.textViewDescription.setTextColor(context.getThemeColorStateList(materialR.attr.colorOutline))
binding.textViewTitle.typeface = Typeface.DEFAULT
binding.textViewDescription.typeface = Typeface.DEFAULT
}

View File

@@ -7,7 +7,6 @@ import android.graphics.Paint
import android.graphics.RectF
import android.view.View
import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
import androidx.recyclerview.widget.RecyclerView
import org.koitharu.kotatsu.R
@@ -20,10 +19,7 @@ import com.google.android.material.R as materialR
class ChaptersSelectionDecoration(context: Context) : AbstractSelectionItemDecoration() {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val radius = context.resources.getDimension(materialR.dimen.abc_control_corner_material)
private val checkIcon = ContextCompat.getDrawable(context, materialR.drawable.ic_mtrl_checked_circle)
private val iconOffset = context.resources.getDimensionPixelOffset(R.dimen.chapter_check_offset)
private val iconSize = context.resources.getDimensionPixelOffset(R.dimen.chapter_check_size)
private val defaultRadius = context.resources.getDimension(materialR.dimen.abc_control_corner_material)
private val strokeColor = context.getThemeColor(materialR.attr.colorPrimary, Color.RED)
private val fillColor = ColorUtils.setAlphaComponent(
ColorUtils.blendARGB(strokeColor, context.getThemeColor(materialR.attr.colorSurface), 0.8f),
@@ -36,12 +32,11 @@ class ChaptersSelectionDecoration(context: Context) : AbstractSelectionItemDecor
98,
)
paint.style = Paint.Style.FILL
hasBackground = true
hasBackground = false
hasForeground = true
isIncludeDecorAndMargins = false
paint.strokeWidth = context.resources.getDimension(R.dimen.selection_stroke_width)
checkIcon?.setTint(strokeColor)
}
override fun getItemId(parent: RecyclerView, child: View): Long {
@@ -50,19 +45,6 @@ class ChaptersSelectionDecoration(context: Context) : AbstractSelectionItemDecor
return item.chapter.id
}
override fun onDrawBackground(
canvas: Canvas,
parent: RecyclerView,
child: View,
bounds: RectF,
state: RecyclerView.State,
) {
if (child is CardView) {
return
}
canvas.drawRoundRect(bounds, radius, radius, paint)
}
override fun onDrawForeground(
canvas: Canvas,
parent: RecyclerView,
@@ -70,24 +52,16 @@ class ChaptersSelectionDecoration(context: Context) : AbstractSelectionItemDecor
bounds: RectF,
state: RecyclerView.State
) {
if (child !is CardView) {
return
val radius = if (child is CardView) {
child.radius
} else {
defaultRadius
}
val radius = child.radius
paint.color = fillColor
paint.style = Paint.Style.FILL
canvas.drawRoundRect(bounds, radius, radius, paint)
paint.color = strokeColor
paint.style = Paint.Style.STROKE
canvas.drawRoundRect(bounds, radius, radius, paint)
checkIcon?.run {
setBounds(
(bounds.right - iconSize - iconOffset).toInt(),
(bounds.top + iconOffset).toInt(),
(bounds.right - iconOffset).toInt(),
(bounds.top + iconOffset + iconSize).toInt(),
)
draw(canvas)
}
}
}

View File

@@ -5,11 +5,13 @@ import org.jsoup.internal.StringUtil.StringJoiner
import org.koitharu.kotatsu.core.model.formatNumber
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.MangaChapter
import kotlin.experimental.and
data class ChapterListItem(
val chapter: MangaChapter,
val flags: Int,
val flags: Byte,
private val uploadDateMs: Long,
private val groupPosition: Byte,
) : ListModel {
var description: String? = null
@@ -51,6 +53,15 @@ data class ChapterListItem(
val isGrid: Boolean
get() = hasFlag(FLAG_GRID)
val isGroupStart: Boolean
get() = (groupPosition and GROUP_START) == GROUP_START
val isGroupMiddle: Boolean
get() = (groupPosition and GROUP_MIDDLE) == GROUP_MIDDLE
val isGroupEnd: Boolean
get() = (groupPosition and GROUP_END) == GROUP_END
private fun buildDescription(): String {
val joiner = StringJoiner("")
chapter.formatNumber()?.let {
@@ -67,7 +78,7 @@ data class ChapterListItem(
return joiner.complete()
}
private fun hasFlag(flag: Int): Boolean {
private fun hasFlag(flag: Byte): Boolean {
return (flags and flag) == flag
}
@@ -88,11 +99,15 @@ data class ChapterListItem(
companion object {
const val FLAG_UNREAD = 2
const val FLAG_CURRENT = 4
const val FLAG_NEW = 8
const val FLAG_BOOKMARKED = 16
const val FLAG_DOWNLOADED = 32
const val FLAG_GRID = 64
const val FLAG_UNREAD: Byte = 2
const val FLAG_CURRENT: Byte = 4
const val FLAG_NEW: Byte = 8
const val FLAG_BOOKMARKED: Byte = 16
const val FLAG_DOWNLOADED: Byte = 32
const val FLAG_GRID: Byte = 64
const val GROUP_START: Byte = 2
const val GROUP_MIDDLE: Byte = 4
const val GROUP_END: Byte = 8
}
}

View File

@@ -7,6 +7,7 @@ import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_GRID
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_NEW
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_UNREAD
import org.koitharu.kotatsu.parsers.model.MangaChapter
import kotlin.experimental.or
fun MangaChapter.toListItem(
isCurrent: Boolean,
@@ -16,7 +17,7 @@ fun MangaChapter.toListItem(
isBookmarked: Boolean,
isGrid: Boolean,
): ChapterListItem {
var flags = 0
var flags: Byte = 0
if (isCurrent) flags = flags or FLAG_CURRENT
if (isUnread) flags = flags or FLAG_UNREAD
if (isNew) flags = flags or FLAG_NEW
@@ -27,5 +28,6 @@ fun MangaChapter.toListItem(
chapter = this,
flags = flags,
uploadDateMs = uploadDate,
groupPosition = 0,
)
}

View File

@@ -7,7 +7,6 @@ import android.graphics.Paint
import android.graphics.RectF
import android.view.View
import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.NO_ID
@@ -21,9 +20,6 @@ import com.google.android.material.R as materialR
open class MangaSelectionDecoration(context: Context) : AbstractSelectionItemDecoration() {
protected val paint = Paint(Paint.ANTI_ALIAS_FLAG)
protected val checkIcon = ContextCompat.getDrawable(context, materialR.drawable.ic_mtrl_checked_circle)
protected val iconOffset = context.resources.getDimensionPixelOffset(R.dimen.card_indicator_offset)
protected val iconSize = context.resources.getDimensionPixelOffset(R.dimen.card_indicator_size)
protected val strokeColor = context.getThemeColor(materialR.attr.colorPrimary, Color.RED)
protected val fillColor = ColorUtils.setAlphaComponent(
ColorUtils.blendARGB(strokeColor, context.getThemeColor(materialR.attr.colorSurface), 0.8f),
@@ -37,7 +33,6 @@ open class MangaSelectionDecoration(context: Context) : AbstractSelectionItemDec
isIncludeDecorAndMargins = false
paint.strokeWidth = context.resources.getDimension(R.dimen.selection_stroke_width)
checkIcon?.setTint(strokeColor)
}
override fun getItemId(parent: RecyclerView, child: View): Long {
@@ -53,7 +48,6 @@ open class MangaSelectionDecoration(context: Context) : AbstractSelectionItemDec
bounds: RectF,
state: RecyclerView.State,
) {
val isCard = child is CardView
val radius = (child as? CardView)?.radius ?: defaultRadius
paint.color = fillColor
paint.style = Paint.Style.FILL
@@ -61,16 +55,5 @@ open class MangaSelectionDecoration(context: Context) : AbstractSelectionItemDec
paint.color = strokeColor
paint.style = Paint.Style.STROKE
canvas.drawRoundRect(bounds, radius, radius, paint)
if (isCard) {
checkIcon?.run {
setBounds(
(bounds.left + iconOffset).toInt(),
(bounds.top + iconOffset).toInt(),
(bounds.left + iconOffset + iconSize).toInt(),
(bounds.top + iconOffset + iconSize).toInt(),
)
draw(canvas)
}
}
}
}

View File

@@ -65,7 +65,7 @@ class ScrobblerConfigActivity : BaseActivity<ActivityScrobblerConfigBinding>(),
processIntent(intent)
}
override fun onNewIntent(intent: Intent?) {
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
if (intent != null) {
setIntent(intent)

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple
xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/selector_overlay">
<item
android:left="@dimen/list_spacing_large"
android:right="@dimen/list_spacing_large">
<shape android:shape="rectangle">
<solid android:color="?colorSurfaceContainerHighest" />
<corners
android:bottomLeftRadius="@dimen/m3_card_corner"
android:bottomRightRadius="@dimen/m3_card_corner" />
<padding
android:left="@dimen/list_spacing_small"
android:right="@dimen/list_spacing_small" />
</shape>
</item>
<item
android:id="@android:id/mask"
android:left="@dimen/list_spacing_large"
android:right="@dimen/list_spacing_large">
<shape android:shape="rectangle">
<solid android:color="@color/selector_overlay" />
<corners
android:bottomLeftRadius="@dimen/m3_card_corner"
android:bottomRightRadius="@dimen/m3_card_corner" />
</shape>
</item>
</ripple>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple
xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/selector_overlay">
<item
android:left="@dimen/list_spacing_large"
android:right="@dimen/list_spacing_large">
<shape android:shape="rectangle">
<solid android:color="?colorSurfaceContainerHighest" />
<corners android:radius="@dimen/m3_card_corner" />
<padding
android:left="@dimen/list_spacing_small"
android:right="@dimen/list_spacing_small" />
</shape>
</item>
<item
android:id="@android:id/mask"
android:left="@dimen/list_spacing_large"
android:right="@dimen/list_spacing_large">
<shape android:shape="rectangle">
<solid android:color="@color/selector_overlay" />
<corners android:radius="@dimen/m3_card_corner" />
</shape>
</item>
</ripple>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple
xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/selector_overlay">
<item
android:left="@dimen/list_spacing_large"
android:right="@dimen/list_spacing_large">
<shape android:shape="rectangle">
<solid android:color="?colorSurfaceContainerHighest" />
<padding
android:left="@dimen/list_spacing_small"
android:right="@dimen/list_spacing_small" />
</shape>
</item>
<item
android:id="@android:id/mask"
android:left="@dimen/list_spacing_large"
android:right="@dimen/list_spacing_large">
<shape android:shape="rectangle">
<solid android:color="@color/selector_overlay" />
</shape>
</item>
</ripple>

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple
xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/selector_overlay">
<item
android:left="@dimen/list_spacing_large"
android:right="@dimen/list_spacing_large">
<shape android:shape="rectangle">
<solid android:color="?colorSurfaceContainerHighest" />
<corners
android:topLeftRadius="@dimen/m3_card_corner"
android:topRightRadius="@dimen/m3_card_corner" />
<padding
android:left="@dimen/list_spacing_small"
android:right="@dimen/list_spacing_small" />
</shape>
</item>
<item
android:id="@android:id/mask"
android:left="@dimen/list_spacing_large"
android:right="@dimen/list_spacing_large">
<shape android:shape="rectangle">
<solid android:color="@color/selector_overlay" />
<corners
android:topLeftRadius="@dimen/m3_card_corner"
android:topRightRadius="@dimen/m3_card_corner" />
</shape>
</item>
</ripple>

View File

@@ -72,6 +72,7 @@
<dimen name="fastscroll_scrollbar_padding_end">6dp</dimen>
<dimen name="m3_side_sheet_width">400dp</dimen>
<dimen name="m3_card_corner">12dp</dimen>
<dimen name="reader_scroll_delta_min">200dp</dimen>