Skip to content

Commit

Permalink
New favorite dialog
Browse files Browse the repository at this point in the history
  • Loading branch information
Koitharu committed Dec 16, 2024
1 parent 1b80e48 commit a5199e2
Show file tree
Hide file tree
Showing 16 changed files with 347 additions and 434 deletions.
18 changes: 18 additions & 0 deletions app/src/main/kotlin/org/koitharu/kotatsu/core/model/MangaSource.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(' ') }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 -> {
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ 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
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
Expand All @@ -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
Expand All @@ -33,26 +34,18 @@ 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(),
refreshTrigger,
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) {
Expand All @@ -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,
)
}
}
}
Loading

0 comments on commit a5199e2

Please sign in to comment.