Improve favorites dialog

This commit is contained in:
Koitharu
2024-08-15 16:14:24 +03:00
parent a61e406c91
commit 28670bc7fb
12 changed files with 258 additions and 67 deletions

View File

@@ -1,6 +1,9 @@
package org.koitharu.kotatsu.core.util.ext package org.koitharu.kotatsu.core.util.ext
import android.content.Context
import androidx.annotation.FloatRange import androidx.annotation.FloatRange
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.parsers.util.ellipsize
import org.koitharu.kotatsu.parsers.util.levenshteinDistance import org.koitharu.kotatsu.parsers.util.levenshteinDistance
import java.util.UUID import java.util.UUID
@@ -40,3 +43,24 @@ fun CharSequence.sanitize(): CharSequence {
} }
fun Char.isReplacement() = this in '\uFFF0'..'\uFFFF' fun Char.isReplacement() = this in '\uFFF0'..'\uFFFF'
fun <T> Collection<T>.joinToStringWithLimit(context: Context, limit: Int, transform: ((T) -> String)): String {
if (size == 1) {
return transform(first()).ellipsize(limit)
}
return buildString(limit + 6) {
for ((i, item) in this@joinToStringWithLimit.withIndex()) {
val str = transform(item)
when {
i == 0 -> append(str.ellipsize(limit - 4))
length + str.length > limit -> {
append(", ")
append(context.getString(R.string.list_ellipsize_pattern, this@joinToStringWithLimit.size - i))
break
}
else -> append(", ").append(str)
}
}
}
}

View File

@@ -68,6 +68,7 @@ import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getThemeColor import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
import org.koitharu.kotatsu.core.util.ext.isTextTruncated import org.koitharu.kotatsu.core.util.ext.isTextTruncated
import org.koitharu.kotatsu.core.util.ext.joinToStringWithLimit
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.parentView import org.koitharu.kotatsu.core.util.ext.parentView
@@ -356,23 +357,7 @@ class DetailsActivity :
chip.text = if (categories.isEmpty()) { chip.text = if (categories.isEmpty()) {
getString(R.string.add_to_favourites) getString(R.string.add_to_favourites)
} else { } else {
if (categories.size == 1) { categories.joinToStringWithLimit(this, FAV_LABEL_LIMIT) { it.title }
categories.first().title.ellipsize(FAV_LABEL_LIMIT)
}
buildString(FAV_LABEL_LIMIT + 6) {
for ((i, cat) in categories.withIndex()) {
if (i == 0) {
append(cat.title.ellipsize(FAV_LABEL_LIMIT - 4))
} else if (length + cat.title.length > FAV_LABEL_LIMIT) {
append(", ")
append(getString(R.string.list_ellipsize_pattern, categories.size - i))
break
} else {
append(", ")
append(cat.title)
}
}
}
} }
} }

View File

@@ -128,9 +128,13 @@ abstract class FavouritesDao {
@Query("SELECT favourite_categories.* FROM favourites LEFT JOIN favourite_categories ON favourite_categories.category_id = favourites.category_id WHERE favourites.manga_id = :mangaId AND favourites.deleted_at = 0") @Query("SELECT favourite_categories.* FROM favourites LEFT JOIN favourite_categories ON favourite_categories.category_id = favourites.category_id WHERE favourites.manga_id = :mangaId AND favourites.deleted_at = 0")
abstract fun observeCategories(mangaId: Long): Flow<List<FavouriteCategoryEntity>> abstract fun observeCategories(mangaId: Long): Flow<List<FavouriteCategoryEntity>>
@Deprecated("")
@Query("SELECT DISTINCT category_id FROM favourites WHERE manga_id IN (:mangaIds) AND deleted_at = 0 ORDER BY favourites.created_at ASC") @Query("SELECT DISTINCT category_id FROM favourites WHERE manga_id IN (:mangaIds) AND deleted_at = 0 ORDER BY favourites.created_at ASC")
abstract suspend fun findCategoriesIds(mangaIds: Collection<Long>): List<Long> abstract suspend fun findCategoriesIds(mangaIds: Collection<Long>): List<Long>
@Query("SELECT DISTINCT category_id FROM favourites WHERE manga_id = :mangaId AND deleted_at = 0 ORDER BY favourites.created_at ASC")
abstract suspend fun findCategoriesIds(mangaId: Long): List<Long>
@Query("SELECT COUNT(category_id) FROM favourites WHERE manga_id = :mangaId AND deleted_at = 0") @Query("SELECT COUNT(category_id) FROM favourites WHERE manga_id = :mangaId AND deleted_at = 0")
abstract suspend fun findCategoriesCount(mangaId: Long): Int abstract suspend fun findCategoriesCount(mangaId: Long): Int

View File

@@ -125,10 +125,15 @@ class FavouritesRepository @Inject constructor(
return db.getFavouritesDao().findCategoriesCount(mangaId) != 0 return db.getFavouritesDao().findCategoriesCount(mangaId) != 0
} }
@Deprecated("")
suspend fun getCategoriesIds(mangaIds: Collection<Long>): Set<Long> { suspend fun getCategoriesIds(mangaIds: Collection<Long>): Set<Long> {
return db.getFavouritesDao().findCategoriesIds(mangaIds).toSet() return db.getFavouritesDao().findCategoriesIds(mangaIds).toSet()
} }
suspend fun getCategoriesIds(mangaId: Long): Set<Long> {
return db.getFavouritesDao().findCategoriesIds(mangaId).toSet()
}
suspend fun createCategory( suspend fun createCategory(
title: String, title: String,
sortOrder: ListSortOrder, sortOrder: ListSortOrder,

View File

@@ -7,6 +7,7 @@ import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import coil.ImageLoader
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
@@ -20,12 +21,16 @@ import org.koitharu.kotatsu.databinding.SheetFavoriteCategoriesBinding
import org.koitharu.kotatsu.favourites.ui.categories.select.adapter.MangaCategoriesAdapter import org.koitharu.kotatsu.favourites.ui.categories.select.adapter.MangaCategoriesAdapter
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class FavoriteSheet : BaseAdaptiveSheet<SheetFavoriteCategoriesBinding>(), OnListItemClickListener<MangaCategoryItem> { class FavoriteSheet : BaseAdaptiveSheet<SheetFavoriteCategoriesBinding>(), OnListItemClickListener<MangaCategoryItem> {
private val viewModel by viewModels<FavoriteSheetViewModel>() private val viewModel by viewModels<FavoriteSheetViewModel>()
@Inject
lateinit var coil: ImageLoader
override fun onCreateViewBinding( override fun onCreateViewBinding(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@@ -36,7 +41,7 @@ class FavoriteSheet : BaseAdaptiveSheet<SheetFavoriteCategoriesBinding>(), OnLis
savedInstanceState: Bundle?, savedInstanceState: Bundle?,
) { ) {
super.onViewBindingCreated(binding, savedInstanceState) super.onViewBindingCreated(binding, savedInstanceState)
val adapter = MangaCategoriesAdapter(this) val adapter = MangaCategoriesAdapter(coil, viewLifecycleOwner, this)
binding.recyclerViewCategories.adapter = adapter binding.recyclerViewCategories.adapter = adapter
viewModel.content.observe(viewLifecycleOwner, adapter) viewModel.content.observe(viewLifecycleOwner, adapter)
viewModel.onError.observeEvent(viewLifecycleOwner, ::onError) viewModel.onError.observeEvent(viewLifecycleOwner, ::onError)

View File

@@ -1,5 +1,7 @@
package org.koitharu.kotatsu.favourites.ui.categories.select package org.koitharu.kotatsu.favourites.ui.categories.select
import androidx.collection.MutableLongObjectMap
import androidx.collection.MutableLongSet
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
@@ -7,19 +9,20 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.ids import org.koitharu.kotatsu.core.model.ids
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.firstNotNull
import org.koitharu.kotatsu.core.util.ext.require import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.favourites.domain.model.Cover
import org.koitharu.kotatsu.favourites.ui.categories.select.model.CategoriesHeaderItem import org.koitharu.kotatsu.favourites.ui.categories.select.model.CategoriesHeaderItem
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.mapToSet
import javax.inject.Inject import javax.inject.Inject
@@ -33,41 +36,52 @@ class FavoriteSheetViewModel @Inject constructor(
private val manga = savedStateHandle.require<List<ParcelableManga>>(FavoriteSheet.KEY_MANGA_LIST).mapToSet { private val manga = savedStateHandle.require<List<ParcelableManga>>(FavoriteSheet.KEY_MANGA_LIST).mapToSet {
it.manga it.manga
} }
private val header = CategoriesHeaderItem() private val header = CategoriesHeaderItem(
private val checkedCategories = MutableStateFlow<Set<Long>?>(null) titles = manga.map { it.title },
covers = manga.take(3).map {
Cover(
url = it.coverUrl,
source = it.source.name,
)
},
)
private val refreshTrigger = MutableStateFlow(Any())
val content = combine( val content = combine(
favouritesRepository.observeCategories(), favouritesRepository.observeCategories(),
checkedCategories.filterNotNull(), refreshTrigger,
settings.observeAsFlow(AppSettings.KEY_TRACKER_ENABLED) { isTrackerEnabled }, settings.observeAsFlow(AppSettings.KEY_TRACKER_ENABLED) { isTrackerEnabled },
) { categories, checked, tracker -> ) { categories, _, tracker ->
buildList(categories.size + 1) { mapList(categories, tracker)
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(header))
fun setChecked(categoryId: Long, isChecked: Boolean) {
launchJob(Dispatchers.Default) {
if (isChecked) {
favouritesRepository.addToCategory(categoryId, manga)
} else {
favouritesRepository.removeFromCategory(categoryId, manga.ids())
}
refreshTrigger.value = Any()
}
}
private suspend fun mapList(categories: List<FavouriteCategory>, tracker: Boolean): List<ListModel> {
val cats = MutableLongObjectMap<MutableLongSet>(categories.size)
categories.forEach { cats[it.id] = MutableLongSet(manga.size) }
for (m in manga) {
val ids = favouritesRepository.getCategoriesIds(m.id)
ids.forEach { id -> cats[id]?.add(m.id) }
}
return buildList(categories.size + 1) {
add(header) add(header)
categories.mapTo(this) { cat -> categories.mapTo(this) { cat ->
MangaCategoryItem( MangaCategoryItem(
category = cat, category = cat,
isChecked = cat.id in checked, isChecked = cats[cat.id]?.isNotEmpty() == true,
isTrackerEnabled = tracker, isTrackerEnabled = tracker,
isEnabled = cats[cat.id]?.let { it.size == 0 || it.size == manga.size } == true,
) )
} }
} }
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(header))
init {
launchJob(Dispatchers.Default) {
checkedCategories.value = favouritesRepository.getCategoriesIds(manga.ids())
}
}
fun setChecked(categoryId: Long, isChecked: Boolean) {
launchJob(Dispatchers.Default) {
val checkedIds = checkedCategories.firstNotNull()
if (isChecked) {
checkedCategories.value = checkedIds + categoryId
favouritesRepository.addToCategory(categoryId, manga)
} else {
checkedCategories.value = checkedIds - categoryId
favouritesRepository.removeFromCategory(categoryId, manga.ids())
}
}
} }
} }

View File

@@ -1,16 +1,32 @@
package org.koitharu.kotatsu.favourites.ui.categories.select.adapter package org.koitharu.kotatsu.favourites.ui.categories.select.adapter
import android.content.Intent import android.content.Intent
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.View import android.view.View
import androidx.core.graphics.ColorUtils
import androidx.core.widget.ImageViewCompat
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.joinToStringWithLimit
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.databinding.ItemCategoriesHeaderBinding import org.koitharu.kotatsu.databinding.ItemCategoriesHeaderBinding
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity
import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity
import org.koitharu.kotatsu.favourites.ui.categories.select.model.CategoriesHeaderItem import org.koitharu.kotatsu.favourites.ui.categories.select.model.CategoriesHeaderItem
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
fun categoriesHeaderAD() = adapterDelegateViewBinding<CategoriesHeaderItem, ListModel, ItemCategoriesHeaderBinding>( fun categoriesHeaderAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
) = adapterDelegateViewBinding<CategoriesHeaderItem, ListModel, ItemCategoriesHeaderBinding>(
{ inflater, parent -> ItemCategoriesHeaderBinding.inflate(inflater, parent, false) }, { inflater, parent -> ItemCategoriesHeaderBinding.inflate(inflater, parent, false) },
) { ) {
@@ -25,4 +41,38 @@ fun categoriesHeaderAD() = adapterDelegateViewBinding<CategoriesHeaderItem, List
binding.chipCreate.setOnClickListener(onClickListener) binding.chipCreate.setOnClickListener(onClickListener)
binding.chipManage.setOnClickListener(onClickListener) binding.chipManage.setOnClickListener(onClickListener)
val backgroundColor = context.getThemeColor(android.R.attr.colorBackground)
ImageViewCompat.setImageTintList(
binding.imageViewCover3,
ColorStateList.valueOf(ColorUtils.setAlphaComponent(backgroundColor, 153)),
)
ImageViewCompat.setImageTintList(
binding.imageViewCover2,
ColorStateList.valueOf(ColorUtils.setAlphaComponent(backgroundColor, 76)),
)
binding.imageViewCover2.backgroundTintList =
ColorStateList.valueOf(ColorUtils.setAlphaComponent(backgroundColor, 76))
binding.imageViewCover3.backgroundTintList =
ColorStateList.valueOf(ColorUtils.setAlphaComponent(backgroundColor, 153))
val fallback = ColorDrawable(Color.TRANSPARENT)
val coverViews = arrayOf(binding.imageViewCover1, binding.imageViewCover2, binding.imageViewCover3)
val crossFadeDuration = context.getAnimationDuration(R.integer.config_defaultAnimTime).toInt()
bind {
binding.textViewTitle.text = item.titles.joinToStringWithLimit(context, 120) { it }
repeat(coverViews.size) { i ->
val cover = item.covers.getOrNull(i)
coverViews[i].newImageRequest(lifecycleOwner, cover?.url)?.run {
placeholder(R.drawable.ic_placeholder)
fallback(fallback)
source(cover?.mangaSource)
crossfade(crossFadeDuration * (i + 1))
error(R.drawable.ic_error_placeholder)
allowRgb565(true)
enqueueWith(coil)
}
}
}
} }

View File

@@ -1,16 +1,20 @@
package org.koitharu.kotatsu.favourites.ui.categories.select.adapter package org.koitharu.kotatsu.favourites.ui.categories.select.adapter
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import org.koitharu.kotatsu.core.ui.BaseListAdapter import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
class MangaCategoriesAdapter( class MangaCategoriesAdapter(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
clickListener: OnListItemClickListener<MangaCategoryItem>, clickListener: OnListItemClickListener<MangaCategoryItem>,
) : BaseListAdapter<ListModel>() { ) : BaseListAdapter<ListModel>() {
init { init {
delegatesManager.addDelegate(mangaCategoryAD(clickListener)) delegatesManager.addDelegate(mangaCategoryAD(clickListener))
.addDelegate(categoriesHeaderAD()) .addDelegate(categoriesHeaderAD(coil, lifecycleOwner))
} }
} }

View File

@@ -21,6 +21,8 @@ fun mangaCategoryAD(
} }
bind { payloads -> bind { payloads ->
binding.root.isEnabled = item.isEnabled
binding.checkableImageView.isEnabled = item.isEnabled
binding.checkableImageView.setChecked(item.isChecked, ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED in payloads) binding.checkableImageView.setChecked(item.isChecked, ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED in payloads)
binding.textViewTitle.text = item.category.title binding.textViewTitle.text = item.category.title
binding.imageViewTracker.isVisible = item.category.isTrackingEnabled && item.isTrackerEnabled binding.imageViewTracker.isVisible = item.category.isTrackingEnabled && item.isTrackerEnabled

View File

@@ -1,8 +1,12 @@
package org.koitharu.kotatsu.favourites.ui.categories.select.model package org.koitharu.kotatsu.favourites.ui.categories.select.model
import org.koitharu.kotatsu.favourites.domain.model.Cover
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
class CategoriesHeaderItem : ListModel { data class CategoriesHeaderItem(
val titles: List<String>,
val covers: List<Cover>,
) : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean { override fun areItemsTheSame(other: ListModel): Boolean {
return other is CategoriesHeaderItem return other is CategoriesHeaderItem

View File

@@ -7,6 +7,7 @@ import org.koitharu.kotatsu.list.ui.model.ListModel
data class MangaCategoryItem( data class MangaCategoryItem(
val category: FavouriteCategory, val category: FavouriteCategory,
val isChecked: Boolean, val isChecked: Boolean,
val isEnabled: Boolean,
val isTrackerEnabled: Boolean, val isTrackerEnabled: Boolean,
) : ListModel { ) : ListModel {

View File

@@ -1,34 +1,127 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<HorizontalScrollView <androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content">
android:scrollbars="none">
<com.google.android.material.chip.ChipGroup <androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_start"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="?listPreferredItemPaddingStart" />
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/imageView_cover3"
android:layout_width="0dp"
android:layout_height="64dp"
android:layout_marginStart="24dp"
android:layout_marginBottom="12dp"
android:background="?attr/colorSecondaryContainer"
android:backgroundTintMode="src_atop"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="@id/guideline"
app:layout_constraintDimensionRatio="W,13:18"
app:layout_constraintStart_toStartOf="@id/guideline_start"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
app:tintMode="src_atop"
tools:backgroundTint="#99FFFFFF"
tools:src="@tools:sample/backgrounds/scenic"
tools:tint="#99FFFFFF" />
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/imageView_cover2"
android:layout_width="0dp"
android:layout_height="64dp"
android:layout_marginStart="12dp"
android:background="?attr/colorSecondaryContainer"
android:backgroundTintMode="src_atop"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="@id/guideline"
app:layout_constraintDimensionRatio="W,13:18"
app:layout_constraintStart_toStartOf="@id/guideline_start"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
app:tintMode="src_atop"
tools:backgroundTint="#4DFFFFFF"
tools:src="@tools:sample/backgrounds/scenic"
tools:tint="#4DFFFFFF" />
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/imageView_cover1"
android:layout_width="0dp"
android:layout_height="64dp"
android:layout_marginTop="12dp"
android:background="?attr/colorSecondaryContainer"
android:backgroundTintMode="src_atop"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="@id/guideline"
app:layout_constraintDimensionRatio="W,13:18"
app:layout_constraintStart_toStartOf="@id/guideline_start"
app:layout_constraintTop_toTopOf="parent"
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="@dimen/margin_normal"
android:layout_marginEnd="?listPreferredItemPaddingEnd"
android:ellipsize="end"
android:textAppearance="?attr/textAppearanceBodyLarge"
app:layout_constrainedHeight="true"
app:layout_constraintBottom_toBottomOf="@id/guideline"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/imageView_cover3"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/lorem[22]" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_begin="92dp" />
<HorizontalScrollView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:clipToPadding="false"
android:paddingStart="?listPreferredItemPaddingStart" android:paddingStart="?listPreferredItemPaddingStart"
android:paddingEnd="?listPreferredItemPaddingEnd" android:paddingEnd="?listPreferredItemPaddingEnd"
app:singleLine="true"> android:scrollbars="none"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/guideline">
<com.google.android.material.chip.Chip <com.google.android.material.chip.ChipGroup
android:id="@+id/chip_create"
style="@style/Widget.Kotatsu.Chip.Assist"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/create_category" app:singleLine="true">
app:chipIcon="@drawable/ic_add" />
<com.google.android.material.chip.Chip <com.google.android.material.chip.Chip
android:id="@+id/chip_manage" android:id="@+id/chip_create"
style="@style/Widget.Kotatsu.Chip.Assist" style="@style/Widget.Kotatsu.Chip.Assist"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/manage_categories" android:text="@string/create_category"
app:chipIcon="@drawable/ic_edit" /> app:chipIcon="@drawable/ic_add" />
</com.google.android.material.chip.ChipGroup> <com.google.android.material.chip.Chip
android:id="@+id/chip_manage"
style="@style/Widget.Kotatsu.Chip.Assist"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/manage_categories"
app:chipIcon="@drawable/ic_edit" />
</HorizontalScrollView> </com.google.android.material.chip.ChipGroup>
</HorizontalScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>