New favorite dialog

This commit is contained in:
Koitharu
2024-12-16 19:17:25 +02:00
parent 1b80e48ed4
commit a5199e2f06
16 changed files with 347 additions and 434 deletions

View File

@@ -2,11 +2,16 @@ package org.koitharu.kotatsu.core.model
import android.content.Context
import android.graphics.Color
import android.os.Build
import android.text.SpannableStringBuilder
import android.text.style.ForegroundColorSpan
import android.text.style.ImageSpan
import android.text.style.RelativeSizeSpan
import android.text.style.SuperscriptSpan
import android.widget.TextView
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import androidx.core.text.inSpans
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.external.ExternalMangaSource
@@ -100,3 +105,16 @@ fun SpannableStringBuilder.appendNsfwLabel(context: Context) = inSpans(
) {
append(context.getString(R.string.nsfw))
}
fun SpannableStringBuilder.appendIcon(textView: TextView, @DrawableRes resId: Int): SpannableStringBuilder {
val icon = ContextCompat.getDrawable(textView.context, resId) ?: return this
icon.setTintList(textView.textColors)
val size = textView.lineHeight
icon.setBounds(0, 0, size, size)
val alignment = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ImageSpan.ALIGN_CENTER
} else {
ImageSpan.ALIGN_BOTTOM
}
return inSpans(ImageSpan(icon, alignment)) { append(' ') }
}

View File

@@ -94,7 +94,7 @@ import org.koitharu.kotatsu.details.ui.scrobbling.ScrobblingItemDecoration
import org.koitharu.kotatsu.details.ui.scrobbling.ScrollingInfoAdapter
import org.koitharu.kotatsu.download.ui.dialog.DownloadDialogFragment
import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver
import org.koitharu.kotatsu.favourites.ui.categories.select.FavoriteSheet
import org.koitharu.kotatsu.favourites.ui.categories.select.FavoriteDialog
import org.koitharu.kotatsu.image.ui.ImageActivity
import org.koitharu.kotatsu.list.domain.MangaListMapper
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
@@ -237,7 +237,7 @@ class DetailsActivity :
R.id.chip_favorite -> {
val manga = viewModel.manga.value ?: return
FavoriteSheet.show(supportFragmentManager, manga)
FavoriteDialog.show(supportFragmentManager, manga)
}
// R.id.chip_time -> {

View File

@@ -0,0 +1,150 @@
package org.koitharu.kotatsu.favourites.ui.categories.select
import android.content.DialogInterface
import android.content.Intent
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.graphics.ColorUtils
import androidx.core.view.isVisible
import androidx.core.widget.ImageViewCompat
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
import coil3.ImageLoader
import coil3.request.allowRgb565
import coil3.request.crossfade
import coil3.request.error
import coil3.request.fallback
import coil3.request.placeholder
import com.google.android.material.checkbox.MaterialCheckBox
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.ui.AlertDialogFragment
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.joinToStringWithLimit
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.showDistinct
import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.SheetFavoriteCategoriesBinding
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity
import org.koitharu.kotatsu.favourites.ui.categories.select.adapter.MangaCategoriesAdapter
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
import org.koitharu.kotatsu.parsers.model.Manga
import javax.inject.Inject
@AndroidEntryPoint
class FavoriteDialog : AlertDialogFragment<SheetFavoriteCategoriesBinding>(),
OnListItemClickListener<MangaCategoryItem>, DialogInterface.OnClickListener {
private val viewModel by viewModels<FavoriteSheetViewModel>()
@Inject
lateinit var coil: ImageLoader
override fun onCreateViewBinding(
inflater: LayoutInflater,
container: ViewGroup?,
) = SheetFavoriteCategoriesBinding.inflate(inflater, container, false)
override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
return super.onBuildDialog(builder)
.setPositiveButton(R.string.done, null)
.setNeutralButton(R.string.manage, this)
}
override fun onViewBindingCreated(
binding: SheetFavoriteCategoriesBinding,
savedInstanceState: Bundle?,
) {
super.onViewBindingCreated(binding, savedInstanceState)
val adapter = MangaCategoriesAdapter(coil, viewLifecycleOwner, this)
binding.recyclerViewCategories.adapter = adapter
viewModel.content.observe(viewLifecycleOwner, adapter)
viewModel.onError.observeEvent(viewLifecycleOwner, ::onError)
bindHeader()
}
override fun onItemClick(item: MangaCategoryItem, view: View) {
viewModel.setChecked(item.category.id, item.checkedState != MaterialCheckBox.STATE_CHECKED)
}
override fun onClick(dialog: DialogInterface?, which: Int) {
startActivity(Intent(context ?: return, FavouriteCategoriesActivity::class.java))
}
private fun onError(e: Throwable) {
Toast.makeText(context ?: return, e.getDisplayMessage(resources), Toast.LENGTH_SHORT).show()
}
private fun bindHeader() {
val manga = viewModel.manga
val binding = viewBinding ?: return
val backgroundColor = binding.root.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 = binding.root.context.getAnimationDuration(R.integer.config_defaultAnimTime).toInt()
binding.textViewTitle.text = manga.joinToStringWithLimit(binding.root.context, 92) { it.title }
repeat(coverViews.size) { i ->
val m = manga.getOrNull(i)
val view = coverViews[i]
view.isVisible = m != null
if (m == null) {
view.disposeImageRequest()
} else {
view.newImageRequest(viewLifecycleOwner, m.coverUrl)?.run {
placeholder(R.drawable.ic_placeholder)
fallback(fallback)
mangaSourceExtra(m.source)
crossfade(crossFadeDuration * (i + 1))
error(R.drawable.ic_error_placeholder)
allowRgb565(true)
enqueueWith(coil)
}
}
}
}
companion object {
private const val TAG = "FavoriteSheet"
const val KEY_MANGA_LIST = "manga_list"
fun show(fm: FragmentManager, manga: Manga) = show(fm, setOf(manga))
fun show(fm: FragmentManager, manga: Collection<Manga>) = FavoriteDialog().withArgs(1) {
putParcelableArrayList(
KEY_MANGA_LIST,
manga.mapTo(ArrayList(manga.size), ::ParcelableManga),
)
}.showDistinct(fm, TAG)
}
}

View File

@@ -1,72 +0,0 @@
package org.koitharu.kotatsu.favourites.ui.categories.select
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
import coil3.ImageLoader
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.showDistinct
import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.SheetFavoriteCategoriesBinding
import org.koitharu.kotatsu.favourites.ui.categories.select.adapter.MangaCategoriesAdapter
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
import org.koitharu.kotatsu.parsers.model.Manga
import javax.inject.Inject
@AndroidEntryPoint
class FavoriteSheet : BaseAdaptiveSheet<SheetFavoriteCategoriesBinding>(), OnListItemClickListener<MangaCategoryItem> {
private val viewModel by viewModels<FavoriteSheetViewModel>()
@Inject
lateinit var coil: ImageLoader
override fun onCreateViewBinding(
inflater: LayoutInflater,
container: ViewGroup?,
) = SheetFavoriteCategoriesBinding.inflate(inflater, container, false)
override fun onViewBindingCreated(
binding: SheetFavoriteCategoriesBinding,
savedInstanceState: Bundle?,
) {
super.onViewBindingCreated(binding, savedInstanceState)
val adapter = MangaCategoriesAdapter(coil, viewLifecycleOwner, this)
binding.recyclerViewCategories.adapter = adapter
viewModel.content.observe(viewLifecycleOwner, adapter)
viewModel.onError.observeEvent(viewLifecycleOwner, ::onError)
}
override fun onItemClick(item: MangaCategoryItem, view: View) {
viewModel.setChecked(item.category.id, !item.isChecked)
}
private fun onError(e: Throwable) {
Toast.makeText(context ?: return, e.getDisplayMessage(resources), Toast.LENGTH_SHORT).show()
}
companion object {
private const val TAG = "FavoriteSheet"
const val KEY_MANGA_LIST = "manga_list"
fun show(fm: FragmentManager, manga: Manga) = show(fm, setOf(manga))
fun show(fm: FragmentManager, manga: Collection<Manga>) = FavoriteSheet().withArgs(1) {
putParcelableArrayList(
KEY_MANGA_LIST,
manga.mapTo(ArrayList(manga.size), ::ParcelableManga),
)
}.showDistinct(fm, TAG)
}
}

View File

@@ -4,6 +4,7 @@ import androidx.collection.MutableLongObjectMap
import androidx.collection.MutableLongSet
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.google.android.material.checkbox.MaterialCheckBox
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
@@ -11,6 +12,7 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.ids
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
@@ -19,11 +21,10 @@ import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.require
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.MangaCategoryItem
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.list.ui.model.LoadingState
import javax.inject.Inject
@HiltViewModel
@@ -33,18 +34,10 @@ class FavoriteSheetViewModel @Inject constructor(
settings: AppSettings,
) : BaseViewModel() {
private val manga = savedStateHandle.require<List<ParcelableManga>>(FavoriteSheet.KEY_MANGA_LIST).mapToSet {
val manga = savedStateHandle.require<List<ParcelableManga>>(FavoriteDialog.KEY_MANGA_LIST).map {
it.manga
}
private val header = CategoriesHeaderItem(
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(
favouritesRepository.observeCategories(),
@@ -52,7 +45,7 @@ class FavoriteSheetViewModel @Inject constructor(
settings.observeAsFlow(AppSettings.KEY_TRACKER_ENABLED) { isTrackerEnabled },
) { categories, _, tracker ->
mapList(categories, tracker)
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(header))
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
fun setChecked(categoryId: Long, isChecked: Boolean) {
launchJob(Dispatchers.Default) {
@@ -66,22 +59,32 @@ class FavoriteSheetViewModel @Inject constructor(
}
private suspend fun mapList(categories: List<FavouriteCategory>, tracker: Boolean): List<ListModel> {
if (categories.isEmpty()) {
return listOf(
EmptyState(
icon = 0,
textPrimary = R.string.empty_favourite_categories,
textSecondary = 0,
actionStringRes = 0,
),
)
}
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)
categories.mapTo(this) { cat ->
MangaCategoryItem(
category = cat,
isChecked = cats[cat.id]?.isNotEmpty() == true,
isTrackerEnabled = tracker,
isEnabled = cats[cat.id]?.let { it.size == 0 || it.size == manga.size } == true,
)
}
return categories.map { cat ->
MangaCategoryItem(
category = cat,
checkedState = when (cats[cat.id]?.size ?: 0) {
0 -> MaterialCheckBox.STATE_UNCHECKED
manga.size -> MaterialCheckBox.STATE_CHECKED
else -> MaterialCheckBox.STATE_INDETERMINATE
},
isTrackerEnabled = tracker,
)
}
}
}

View File

@@ -1,91 +0,0 @@
package org.koitharu.kotatsu.favourites.ui.categories.select.adapter
import android.content.Intent
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.View
import androidx.core.graphics.ColorUtils
import androidx.core.view.isVisible
import androidx.core.widget.ImageViewCompat
import androidx.lifecycle.LifecycleOwner
import coil3.ImageLoader
import coil3.request.allowRgb565
import coil3.request.crossfade
import coil3.request.error
import coil3.request.fallback
import coil3.request.placeholder
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
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.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.databinding.ItemCategoriesHeaderBinding
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity
import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity
import org.koitharu.kotatsu.favourites.ui.categories.select.model.CategoriesHeaderItem
import org.koitharu.kotatsu.list.ui.model.ListModel
fun categoriesHeaderAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
) = adapterDelegateViewBinding<CategoriesHeaderItem, ListModel, ItemCategoriesHeaderBinding>(
{ inflater, parent -> ItemCategoriesHeaderBinding.inflate(inflater, parent, false) },
) {
val onClickListener = View.OnClickListener { v ->
val intent = when (v.id) {
R.id.chip_create -> FavouritesCategoryEditActivity.newIntent(v.context)
R.id.chip_manage -> Intent(v.context, FavouriteCategoriesActivity::class.java)
else -> return@OnClickListener
}
v.context.startActivity(intent)
}
binding.chipCreate.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)
val view = coverViews[i]
view.isVisible = cover != null
if (cover == null) {
view.disposeImageRequest()
} else {
view.newImageRequest(lifecycleOwner, cover.url)?.run {
placeholder(R.drawable.ic_placeholder)
fallback(fallback)
mangaSourceExtra(cover.mangaSource)
crossfade(crossFadeDuration * (i + 1))
error(R.drawable.ic_error_placeholder)
allowRgb565(true)
enqueueWith(coil)
}
}
}
}
}

View File

@@ -5,6 +5,9 @@ import coil3.ImageLoader
import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
import org.koitharu.kotatsu.list.ui.model.ListModel
class MangaCategoriesAdapter(
@@ -14,7 +17,8 @@ class MangaCategoriesAdapter(
) : BaseListAdapter<ListModel>() {
init {
delegatesManager.addDelegate(mangaCategoryAD(clickListener))
.addDelegate(categoriesHeaderAD(coil, lifecycleOwner))
addDelegate(ListItemType.NAV_ITEM, mangaCategoryAD(clickListener))
addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, null))
}
}

View File

@@ -1,10 +1,10 @@
package org.koitharu.kotatsu.favourites.ui.categories.select.adapter
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.text.buildSpannedString
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.appendIcon
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.setChecked
import org.koitharu.kotatsu.databinding.ItemCategoryCheckableBinding
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
@@ -21,11 +21,20 @@ fun mangaCategoryAD(
}
bind { payloads ->
binding.root.isEnabled = item.isEnabled
binding.checkableImageView.isEnabled = item.isEnabled
binding.checkableImageView.setChecked(item.isChecked, ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED in payloads)
binding.textViewTitle.text = item.category.title
binding.imageViewTracker.isVisible = item.category.isTrackingEnabled && item.isTrackerEnabled
binding.imageViewHidden.isGone = item.category.isVisibleInLibrary
binding.checkBox.checkedState = item.checkedState
if (ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED !in payloads) {
binding.checkBox.text = buildSpannedString {
append(item.category.title)
if (item.isTrackerEnabled && item.category.isTrackingEnabled) {
append(' ')
appendIcon(binding.checkBox, R.drawable.ic_notification)
}
if (!item.category.isVisibleInLibrary) {
append(' ')
appendIcon(binding.checkBox, R.drawable.ic_eye_off)
}
}
binding.checkBox.jumpDrawablesToCurrentState()
}
}
}

View File

@@ -1,23 +0,0 @@
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
data class CategoriesHeaderItem(
val titles: List<String>,
val covers: List<Cover>,
) : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean {
return other is CategoriesHeaderItem
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
return javaClass == other?.javaClass
}
override fun hashCode(): Int {
return javaClass.hashCode()
}
}

View File

@@ -1,13 +1,13 @@
package org.koitharu.kotatsu.favourites.ui.categories.select.model
import com.google.android.material.checkbox.MaterialCheckBox.CheckedState
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel
data class MangaCategoryItem(
val category: FavouriteCategory,
val isChecked: Boolean,
val isEnabled: Boolean,
@CheckedState val checkedState: Int,
val isTrackerEnabled: Boolean,
) : ListModel {
@@ -16,7 +16,7 @@ data class MangaCategoryItem(
}
override fun getChangePayload(previousState: ListModel): Any? {
return if (previousState is MangaCategoryItem && previousState.isChecked != isChecked) {
return if (previousState is MangaCategoryItem && previousState.checkedState != checkedState) {
ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED
} else {
super.getChangePayload(previousState)

View File

@@ -46,7 +46,7 @@ import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
import org.koitharu.kotatsu.databinding.FragmentListBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.download.ui.dialog.DownloadDialogFragment
import org.koitharu.kotatsu.favourites.ui.categories.select.FavoriteSheet
import org.koitharu.kotatsu.favourites.ui.categories.select.FavoriteDialog
import org.koitharu.kotatsu.list.domain.ListFilterOption
import org.koitharu.kotatsu.list.domain.QuickFilterListener
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
@@ -153,11 +153,11 @@ abstract class MangaListFragment :
}
override fun onItemLongClick(item: Manga, view: View): Boolean {
return selectionController?.onItemLongClick(view, item.id) ?: false
return selectionController?.onItemLongClick(view, item.id) == true
}
override fun onItemContextClick(item: Manga, view: View): Boolean {
return selectionController?.onItemContextClick(view, item.id) ?: false
return selectionController?.onItemContextClick(view, item.id) == true
}
override fun onReadClick(manga: Manga, view: View) {
@@ -317,7 +317,7 @@ abstract class MangaListFragment :
}
R.id.action_favourite -> {
FavoriteSheet.show(getChildFragmentManager(), selectedItems)
FavoriteDialog.show(getChildFragmentManager(), selectedItems)
mode?.finish()
true
}

View File

@@ -1,8 +1,10 @@
package org.koitharu.kotatsu.list.ui.adapter
import androidx.core.view.isVisible
import androidx.lifecycle.LifecycleOwner
import coil3.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
@@ -23,7 +25,13 @@ fun emptyStateListAD(
}
bind {
binding.icon.newImageRequest(lifecycleOwner, item.icon)?.enqueueWith(coil)
if (item.icon == 0) {
binding.icon.isVisible = false
binding.icon.disposeImageRequest()
} else {
binding.icon.isVisible = true
binding.icon.newImageRequest(lifecycleOwner, item.icon)?.enqueueWith(coil)
}
binding.textPrimary.setText(item.textPrimary)
binding.textSecondary.setTextAndVisible(item.textSecondary)
if (listener != null) {

View File

@@ -27,7 +27,7 @@ import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.databinding.ActivitySearchBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.download.ui.dialog.DownloadDialogFragment
import org.koitharu.kotatsu.favourites.ui.categories.select.FavoriteSheet
import org.koitharu.kotatsu.favourites.ui.categories.select.FavoriteDialog
import org.koitharu.kotatsu.list.domain.ListFilterOption
import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
@@ -179,7 +179,7 @@ class SearchActivity :
}
R.id.action_favourite -> {
FavoriteSheet.show(supportFragmentManager, collectSelectedItems())
FavoriteDialog.show(supportFragmentManager, collectSelectedItems())
mode?.finish()
true
}

View File

@@ -1,135 +0,0 @@
<?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">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_start"
android:layout_width="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/imageView_cover2"
app:layout_constraintDimensionRatio="W,13:18"
app:layout_constraintStart_toStartOf="@id/guideline_start"
app:layout_constraintTop_toTopOf="@id/imageView_cover2"
app:layout_goneMarginTop="0dp"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
tools:src="@tools:sample/backgrounds/scenic" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier_covers"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="end"
app:constraint_referenced_ids="imageView_cover1,imageView_cover2,imageView_cover3" />
<TextView
android:id="@+id/textView_title"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="@dimen/margin_normal"
android:layout_marginEnd="?listPreferredItemPaddingEnd"
android:ellipsize="end"
android:gravity="center_vertical|start"
android:textAppearance="?attr/textAppearanceBodyLarge"
app:layout_constraintBottom_toBottomOf="@id/guideline"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/barrier_covers"
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:paddingEnd="?listPreferredItemPaddingEnd"
android:scrollbars="none"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/guideline">
<com.google.android.material.chip.ChipGroup
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:singleLine="true">
<com.google.android.material.chip.Chip
android:id="@+id/chip_create"
style="@style/Widget.Kotatsu.Chip.Assist"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/create_category"
app:chipIcon="@drawable/ic_add" />
<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" />
</com.google.android.material.chip.ChipGroup>
</HorizontalScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,66 +1,22 @@
<?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:id="@+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:gravity="center_vertical"
android:minHeight="?listPreferredItemHeightSmall"
android:paddingStart="?android:listPreferredItemPaddingStart"
android:paddingEnd="?android:listPreferredItemPaddingEnd"
tools:ignore="RtlSymmetry">
android:orientation="vertical"
android:paddingStart="?listPreferredItemPaddingStart"
android:paddingEnd="?listPreferredItemPaddingEnd">
<org.koitharu.kotatsu.core.ui.widgets.CheckableImageView
android:id="@+id/checkableImageView"
android:layout_width="wrap_content"
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/checkBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="?android:listChoiceIndicatorMultiple" />
android:clickable="false"
android:gravity="center_vertical"
tools:checkedState="indeterminate"
tools:text="@tools:sample/cities" />
<TextView
android:id="@+id/textView_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="?listPreferredItemPaddingStart"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="?attr/textAppearanceBodyMedium"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/imageView_tracker"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@id/checkableImageView"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/lorem[1]" />
<ImageView
android:id="@+id/imageView_tracker"
android:layout_width="14dp"
android:layout_height="14dp"
android:layout_marginStart="4dp"
android:contentDescription="@string/check_for_new_chapters"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/imageView_hidden"
app:layout_constraintStart_toEndOf="@id/textView_title"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_notification" />
<ImageView
android:id="@+id/imageView_hidden"
android:layout_width="14dp"
android:layout_height="14dp"
android:layout_marginStart="4dp"
android:contentDescription="@string/show_on_shelf"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/imageView_tracker"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_eye_off" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>

View File

@@ -1,30 +1,116 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
<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:orientation="vertical">
android:paddingTop="@dimen/margin_normal">
<org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetHeaderBar
android:id="@+id/headerBar"
android:layout_width="match_parent"
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_scrollFlags="noScroll"
app:title="@string/add_to_favourites" />
android:orientation="vertical"
app:layout_constraintGuide_begin="@dimen/margin_normal" />
<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/imageView_cover2"
app:layout_constraintDimensionRatio="W,13:18"
app:layout_constraintStart_toStartOf="@id/guideline_start"
app:layout_constraintTop_toTopOf="@id/imageView_cover2"
app:layout_goneMarginTop="0dp"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
tools:src="@tools:sample/backgrounds/scenic" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier_covers"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="end"
app:constraint_referenced_ids="imageView_cover1,imageView_cover2,imageView_cover3" />
<TextView
android:id="@+id/textView_title"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="@dimen/margin_normal"
android:layout_marginEnd="@dimen/margin_normal"
android:ellipsize="end"
android:gravity="center_vertical|start"
android:textAppearance="?attr/textAppearanceBodyLarge"
app:layout_constraintBottom_toBottomOf="@id/guideline"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/barrier_covers"
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" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView_categories"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:clipToPadding="false"
android:orientation="vertical"
android:paddingBottom="@dimen/list_spacing"
android:scrollIndicators="top"
android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/guideline"
tools:ignore="UnusedAttribute"
tools:listitem="@layout/item_checkable_new" />
tools:listitem="@layout/item_category_checkable" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>