Manga preview in list on tablet
This commit is contained in:
@@ -135,7 +135,9 @@ abstract class MangaListFragment :
|
||||
|
||||
override fun onItemClick(item: Manga, view: View) {
|
||||
if (selectionController?.onItemClick(item.id) != true) {
|
||||
startActivity(DetailsActivity.newIntent(context ?: return, item))
|
||||
if ((activity as? MangaListActivity)?.showPreview(item) != true) {
|
||||
startActivity(DetailsActivity.newIntent(context ?: return, item))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
package org.koitharu.kotatsu.list.ui.preview
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.viewModels
|
||||
import coil.ImageLoader
|
||||
import coil.request.ImageRequest
|
||||
import coil.request.SuccessResult
|
||||
import coil.util.CoilUtils
|
||||
import com.google.android.material.chip.Chip
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.BaseFragment
|
||||
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
|
||||
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
import org.koitharu.kotatsu.core.util.ext.crossfade
|
||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
|
||||
import org.koitharu.kotatsu.core.util.ext.textAndVisible
|
||||
import org.koitharu.kotatsu.databinding.FragmentPreviewBinding
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.filter.ui.FilterOwner
|
||||
import org.koitharu.kotatsu.filter.ui.model.FilterItem
|
||||
import org.koitharu.kotatsu.image.ui.ImageActivity
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
||||
import org.koitharu.kotatsu.search.ui.SearchActivity
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class PreviewFragment : BaseFragment<FragmentPreviewBinding>(), View.OnClickListener, ChipsView.OnChipClickListener {
|
||||
|
||||
@Inject
|
||||
lateinit var coil: ImageLoader
|
||||
|
||||
private val viewModel: PreviewViewModel by viewModels()
|
||||
|
||||
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentPreviewBinding {
|
||||
return FragmentPreviewBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewBindingCreated(binding: FragmentPreviewBinding, savedInstanceState: Bundle?) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
binding.buttonClose.isVisible = activity is MangaListActivity
|
||||
binding.buttonClose.setOnClickListener(this)
|
||||
binding.textViewDescription.movementMethod = LinkMovementMethod.getInstance()
|
||||
binding.chipsTags.onChipClickListener = this
|
||||
binding.textViewAuthor.setOnClickListener(this)
|
||||
binding.imageViewCover.setOnClickListener(this)
|
||||
binding.buttonOpen.setOnClickListener(this)
|
||||
|
||||
viewModel.manga.observe(viewLifecycleOwner, ::onMangaUpdated)
|
||||
viewModel.tagsChips.observe(viewLifecycleOwner, ::onTagsChipsChanged)
|
||||
viewModel.description.observe(viewLifecycleOwner, ::onDescriptionChanged)
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
val manga = viewModel.manga.value
|
||||
when (v.id) {
|
||||
R.id.button_close -> closeSelf()
|
||||
R.id.button_open -> startActivity(
|
||||
DetailsActivity.newIntent(v.context, manga),
|
||||
scaleUpActivityOptionsOf(requireView()),
|
||||
)
|
||||
|
||||
R.id.textView_author -> startActivity(
|
||||
SearchActivity.newIntent(
|
||||
context = v.context,
|
||||
source = manga.source,
|
||||
query = manga.author ?: return,
|
||||
),
|
||||
)
|
||||
|
||||
R.id.imageView_cover -> startActivity(
|
||||
ImageActivity.newIntent(
|
||||
v.context,
|
||||
manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl },
|
||||
manga.source,
|
||||
),
|
||||
scaleUpActivityOptionsOf(v),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) = Unit
|
||||
|
||||
override fun onChipClick(chip: Chip, data: Any?) {
|
||||
val tag = data as? MangaTag ?: return
|
||||
val filter = (activity as? FilterOwner)?.filter
|
||||
if (filter == null) {
|
||||
startActivity(MangaListActivity.newIntent(requireContext(), setOf(tag)))
|
||||
} else {
|
||||
filter.onTagItemClick(FilterItem.Tag(tag, false))
|
||||
closeSelf()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onMangaUpdated(manga: Manga) {
|
||||
with(requireViewBinding()) {
|
||||
// Main
|
||||
loadCover(manga)
|
||||
textViewTitle.text = manga.title
|
||||
textViewSubtitle.textAndVisible = manga.altTitle
|
||||
textViewAuthor.textAndVisible = manga.author
|
||||
if (manga.hasRating) {
|
||||
ratingBar.rating = manga.rating * ratingBar.numStars
|
||||
ratingBar.isVisible = true
|
||||
} else {
|
||||
ratingBar.isVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onDescriptionChanged(description: CharSequence?) {
|
||||
val tv = requireViewBinding().textViewDescription
|
||||
if (description.isNullOrBlank()) {
|
||||
tv.setText(R.string.no_description)
|
||||
} else {
|
||||
tv.text = description
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadCover(manga: Manga) {
|
||||
val imageUrl = manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl }
|
||||
val lastResult = CoilUtils.result(requireViewBinding().imageViewCover)
|
||||
if (lastResult is SuccessResult && lastResult.request.data == imageUrl) {
|
||||
return
|
||||
}
|
||||
val request = ImageRequest.Builder(context ?: return)
|
||||
.target(requireViewBinding().imageViewCover)
|
||||
.size(CoverSizeResolver(requireViewBinding().imageViewCover))
|
||||
.data(imageUrl)
|
||||
.tag(manga.source)
|
||||
.crossfade(requireContext())
|
||||
.lifecycle(viewLifecycleOwner)
|
||||
.placeholderMemoryCacheKey(manga.coverUrl)
|
||||
val previousDrawable = lastResult?.drawable
|
||||
if (previousDrawable != null) {
|
||||
request.fallback(previousDrawable)
|
||||
.placeholder(previousDrawable)
|
||||
.error(previousDrawable)
|
||||
} else {
|
||||
request.fallback(R.drawable.ic_placeholder)
|
||||
.placeholder(R.drawable.ic_placeholder)
|
||||
.error(R.drawable.ic_error_placeholder)
|
||||
}
|
||||
request.enqueueWith(coil)
|
||||
}
|
||||
|
||||
private fun onTagsChipsChanged(chips: List<ChipsView.ChipModel>) {
|
||||
requireViewBinding().chipsTags.setChips(chips)
|
||||
}
|
||||
|
||||
private fun closeSelf() {
|
||||
((activity as? MangaListActivity)?.hidePreview())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package org.koitharu.kotatsu.list.ui.preview
|
||||
|
||||
import android.text.Html
|
||||
import android.text.SpannableString
|
||||
import android.text.Spanned
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import androidx.core.text.getSpans
|
||||
import androidx.core.text.parseAsHtml
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.core.parser.MangaIntent
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
import org.koitharu.kotatsu.core.util.ext.require
|
||||
import org.koitharu.kotatsu.core.util.ext.sanitize
|
||||
import org.koitharu.kotatsu.list.domain.ListExtraProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class PreviewViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val extraProvider: ListExtraProvider,
|
||||
private val repositoryFactory: MangaRepository.Factory,
|
||||
private val imageGetter: Html.ImageGetter,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val manga = MutableStateFlow(
|
||||
savedStateHandle.require<ParcelableManga>(MangaIntent.KEY_MANGA).manga,
|
||||
)
|
||||
|
||||
val description = manga
|
||||
.distinctUntilChangedBy { it.description.orEmpty() }
|
||||
.transformLatest {
|
||||
val description = it.description
|
||||
if (description.isNullOrEmpty()) {
|
||||
emit(null)
|
||||
} else {
|
||||
emit(description.parseAsHtml().filterSpans().sanitize())
|
||||
emit(description.parseAsHtml(imageGetter = imageGetter).filterSpans())
|
||||
}
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.WhileSubscribed(5000), null)
|
||||
|
||||
val tagsChips = manga.map {
|
||||
it.tags.map { tag ->
|
||||
ChipsView.ChipModel(
|
||||
title = tag.title,
|
||||
tint = extraProvider.getTagTint(tag),
|
||||
icon = 0,
|
||||
data = tag,
|
||||
isCheckable = false,
|
||||
isChecked = false,
|
||||
)
|
||||
}
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
init {
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
val repo = repositoryFactory.create(manga.value.source)
|
||||
manga.value = repo.getDetails(manga.value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Spanned.filterSpans(): CharSequence {
|
||||
val spannable = SpannableString.valueOf(this)
|
||||
val spans = spannable.getSpans<ForegroundColorSpan>()
|
||||
for (span in spans) {
|
||||
spannable.removeSpan(span)
|
||||
}
|
||||
return spannable.trim()
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,10 @@ import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup.MarginLayoutParams
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.commit
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
@@ -15,7 +17,9 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaTags
|
||||
import org.koitharu.kotatsu.core.parser.MangaIntent
|
||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.core.ui.model.titleRes
|
||||
import org.koitharu.kotatsu.core.util.ext.getParcelableExtraCompat
|
||||
@@ -27,8 +31,10 @@ import org.koitharu.kotatsu.filter.ui.FilterHeaderFragment
|
||||
import org.koitharu.kotatsu.filter.ui.FilterOwner
|
||||
import org.koitharu.kotatsu.filter.ui.FilterSheetFragment
|
||||
import org.koitharu.kotatsu.filter.ui.MangaFilter
|
||||
import org.koitharu.kotatsu.list.ui.preview.PreviewFragment
|
||||
import org.koitharu.kotatsu.local.ui.LocalListFragment
|
||||
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
|
||||
@@ -66,8 +72,9 @@ class MangaListActivity :
|
||||
left = insets.left,
|
||||
right = insets.right,
|
||||
)
|
||||
viewBinding.cardFilter?.updateLayoutParams<MarginLayoutParams> {
|
||||
viewBinding.cardSide?.updateLayoutParams<MarginLayoutParams> {
|
||||
bottomMargin = marginStart + insets.bottom
|
||||
topMargin = marginStart + insets.top
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +84,13 @@ class MangaListActivity :
|
||||
}
|
||||
}
|
||||
|
||||
fun showPreview(manga: Manga): Boolean = setSideFragment(
|
||||
PreviewFragment::class.java,
|
||||
bundleOf(MangaIntent.KEY_MANGA to ParcelableManga(manga, true)),
|
||||
)
|
||||
|
||||
fun hidePreview() = setSideFragment(FilterSheetFragment::class.java, null)
|
||||
|
||||
private fun initList(source: MangaSource, tags: Set<MangaTag>?) {
|
||||
val fm = supportFragmentManager
|
||||
val existingFragment = fm.findFragmentById(R.id.container)
|
||||
@@ -100,12 +114,9 @@ class MangaListActivity :
|
||||
}
|
||||
|
||||
private fun initFilter(filterOwner: FilterOwner) {
|
||||
if (viewBinding.containerFilter != null) {
|
||||
if (supportFragmentManager.findFragmentById(R.id.container_filter) == null) {
|
||||
supportFragmentManager.commit {
|
||||
setReorderingAllowed(true)
|
||||
replace(R.id.container_filter, FilterSheetFragment::class.java, null)
|
||||
}
|
||||
if (viewBinding.containerSide != null) {
|
||||
if (supportFragmentManager.findFragmentById(R.id.container_side) == null) {
|
||||
setSideFragment(FilterSheetFragment::class.java, null)
|
||||
}
|
||||
} else if (viewBinding.containerFilterHeader != null) {
|
||||
if (supportFragmentManager.findFragmentById(R.id.container_filter_header) == null) {
|
||||
@@ -135,6 +146,16 @@ class MangaListActivity :
|
||||
return supportFragmentManager.findFragmentById(R.id.container) as? FilterOwner
|
||||
}
|
||||
|
||||
private fun setSideFragment(cls: Class<out Fragment>, args: Bundle?) = if (viewBinding.containerSide != null) {
|
||||
supportFragmentManager.commit {
|
||||
setReorderingAllowed(true)
|
||||
replace(R.id.container_side, cls, args)
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
private class ApplyFilterRunnable(
|
||||
private val filterOwner: FilterOwner,
|
||||
private val tags: Set<MangaTag>,
|
||||
|
||||
Reference in New Issue
Block a user