From 6bb8ae0d1eed1de468a920412b3f0e600c5d1730 Mon Sep 17 00:00:00 2001 From: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com> Date: Sat, 9 Oct 2021 22:02:45 +0700 Subject: [PATCH] Manga description adjustments (#6011) * Manga description adjustments - Animated state changes - Adjust scrim position to fully show 2 lines when shrunk - Set minLines to avoid scrim hiding oneliner * Change icon and adjust animation * Revert fancy scrim animation (cherry picked from commit f32f1eeaa547dbf3a7a6d0069ee6332d8a440fe7) # Conflicts: # app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt # app/src/main/res/layout-sw720dp/manga_info_header.xml # app/src/main/res/layout/manga_info_header.xml --- .../ui/manga/info/MangaInfoHeaderAdapter.kt | 272 ++++------------ .../ui/manga/info/NamespaceTagsAdapter.kt | 8 - .../ui/manga/info/NamespaceTagsHolder.kt | 9 +- .../ui/manga/info/NamespaceTagsItem.kt | 11 +- .../tachiyomi/widget/MangaSummaryView.kt | 297 ++++++++++++++++++ app/src/main/res/drawable/anim_caret_down.xml | 85 +++++ app/src/main/res/drawable/anim_caret_up.xml | 84 +++++ .../res/layout-sw720dp/manga_info_header.xml | 135 +------- app/src/main/res/layout/manga_info_header.xml | 131 +------- app/src/main/res/layout/manga_summary.xml | 107 +++++++ 10 files changed, 650 insertions(+), 489 deletions(-) delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/NamespaceTagsAdapter.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/widget/MangaSummaryView.kt create mode 100644 app/src/main/res/drawable/anim_caret_down.xml create mode 100644 app/src/main/res/drawable/anim_caret_up.xml create mode 100644 app/src/main/res/layout/manga_summary.xml diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt index 03bdd53da..0fb08745a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt @@ -8,7 +8,6 @@ import androidx.core.view.updateLayoutParams import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.dialog.MaterialAlertDialogBuilder -import eu.davidea.flexibleadapter.FlexibleAdapter import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.preference.PreferencesHelper @@ -21,17 +20,14 @@ import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.MetadataSource import eu.kanade.tachiyomi.ui.base.controller.getMainAppBarHeight import eu.kanade.tachiyomi.ui.manga.MangaController -import eu.kanade.tachiyomi.util.lang.launchUI import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.view.loadAnyAutoPause -import eu.kanade.tachiyomi.util.view.setChips import exh.merged.sql.models.MergedMangaReference import exh.metadata.metadata.base.RaisedSearchMetadata import exh.source.MERGED_SOURCE_ID import exh.source.getMainSource import exh.util.SourceTagsUtil import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach import reactivecircus.flowbinding.android.view.clicks import reactivecircus.flowbinding.android.view.longClicks @@ -58,17 +54,17 @@ class MangaInfoHeaderAdapter( // SY <-- private var trackCount: Int = 0 private var metaInfoAdapter: RecyclerView.Adapter<*>? = null - private var mangaTagsInfoAdapter: NamespaceTagsAdapter? = NamespaceTagsAdapter(controller, source) private lateinit var binding: MangaInfoHeaderBinding - private var initialLoad: Boolean = true - - private val maxLines = 3 - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeaderViewHolder { binding = MangaInfoHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false) updateCoverPosition() + + // Expand manga info if navigated from source listing or explicitly set to + // (e.g. on tablets) + binding.mangaSummarySection.expanded = fromSource || isTablet + // SY --> metaInfoAdapter = source.getMainSource>()?.getDescriptionAdapter(controller) binding.metadataView.isVisible = if (metaInfoAdapter != null) { @@ -78,9 +74,6 @@ class MangaInfoHeaderAdapter( } else { false } - - binding.genreGroups.layoutManager = LinearLayoutManager(binding.root.context) - binding.genreGroups.adapter = mangaTagsInfoAdapter // SY <-- return HeaderViewHolder(binding.root) @@ -132,15 +125,6 @@ class MangaInfoHeaderAdapter( // For rounded corners binding.mangaCover.clipToOutline = true - // SY --> - mangaTagsInfoAdapter?.mItemClickListener = FlexibleAdapter.OnItemClickListener { _, _ -> - controller.viewScope.launchUI { - toggleMangaInfo() - } - false - } - // SY <-- - binding.btnFavorite.clicks() .onEach { controller.onFavoriteClick() } .launchIn(controller.viewScope) @@ -252,15 +236,6 @@ class MangaInfoHeaderAdapter( } .launchIn(controller.viewScope) - binding.mangaSummaryText.longClicks() - .onEach { - controller.activity?.copyToClipboard( - view.context.getString(R.string.description), - binding.mangaSummaryText.text.toString() - ) - } - .launchIn(controller.viewScope) - binding.mangaCover.clicks() .onEach { controller.showFullCoverDialog() @@ -273,7 +248,7 @@ class MangaInfoHeaderAdapter( } .launchIn(controller.viewScope) - setMangaInfo(manga, source, meta) + setMangaInfo() } private fun showCoverOptionsDialog() { @@ -303,7 +278,7 @@ class MangaInfoHeaderAdapter( * @param manga manga object containing information about manga. * @param source the source of the manga. */ - private fun setMangaInfo(manga: Manga, source: Source?, meta: RaisedSearchMetadata?) { + private fun setMangaInfo() { // Update full title TextView. binding.mangaFullTitle.text = if (manga.title.isBlank()) { view.context.getString(R.string.unknown) @@ -326,40 +301,36 @@ class MangaInfoHeaderAdapter( } // If manga source is known update source TextView. - val mangaSource = source?.toString() + val mangaSource = source.toString() with(binding.mangaSource) { - if (mangaSource != null) { - val enabledLanguages = preferences.enabledLanguages().get() - .filterNot { it in listOf("all", "other") } + val enabledLanguages = preferences.enabledLanguages().get() + .filterNot { it in listOf("all", "other") } + // SY --> + val isMergedSource = source?.id == MERGED_SOURCE_ID + // SY <-- + val hasOneActiveLanguages = enabledLanguages.size == 1 + val isInEnabledLanguages = source.lang in enabledLanguages + text = when { // SY --> - val isMergedSource = source.id == MERGED_SOURCE_ID + isMergedSource && hasOneActiveLanguages -> getMergedSourcesString( + enabledLanguages, + true + ) + isMergedSource -> getMergedSourcesString( + enabledLanguages, + false + ) // SY <-- - val hasOneActiveLanguages = enabledLanguages.size == 1 - val isInEnabledLanguages = source.lang in enabledLanguages - text = when { - // SY --> - isMergedSource && hasOneActiveLanguages -> getMergedSourcesString( - enabledLanguages, - true - ) - isMergedSource -> getMergedSourcesString( - enabledLanguages, - false - ) - // SY <-- - // For edge cases where user disables a source they got manga of in their library. - hasOneActiveLanguages && !isInEnabledLanguages -> mangaSource - // Hide the language tag when only one language is used. - hasOneActiveLanguages && isInEnabledLanguages -> source.name - else -> mangaSource - } + // For edge cases where user disables a source they got manga of in their library. + hasOneActiveLanguages && !isInEnabledLanguages -> mangaSource + // Hide the language tag when only one language is used. + hasOneActiveLanguages && isInEnabledLanguages -> source.name + else -> mangaSource + } - setOnClickListener { - controller.performSearch(sourceManager.getOrStub(source.id).name) - } - } else { - text = view.context.getString(R.string.unknown) + setOnClickListener { + controller.performSearch(sourceManager.getOrStub(source.id).name) } } @@ -386,102 +357,35 @@ class MangaInfoHeaderAdapter( binding.mangaCover.loadAnyAutoPause(manga) // Manga info section - val hasInfoContent = !manga.description.isNullOrBlank() || !manga.genre.isNullOrBlank() - showMangaInfo(hasInfoContent) - if (hasInfoContent) { - // Update description TextView. - binding.mangaSummaryText.text = updateDescription(manga.description, (fromSource || isTablet).not()) - - // Update genres list - if (!manga.genre.isNullOrBlank()) { - binding.mangaGenresTagsCompactChips.setChips( - manga.getGenres(), - controller::performGenreSearch - ) - // SY --> - // if (source?.getMainSource() != null) { - setChipsWithNamespace( - manga.getGenres(), - meta - ) - // binding.mangaGenresTagsFullChips.isVisible = false - /*} else { - binding.mangaGenresTagsFullChips.setChips( - manga.getGenres(), - controller::performGenreSearch - ) - binding.genreGroups.isVisible = false - }*/ - // SY <-- - } else { - binding.mangaGenresTagsCompact.isVisible = false - binding.mangaGenresTagsCompactChips.isVisible = false - // binding.mangaGenresTagsFullChips.isVisible = false - // SY --> - binding.genreGroups.isVisible = false - // SY <-- - } - - // Handle showing more or less info - merge( - binding.mangaSummaryText.clicks(), - binding.mangaInfoToggleMore.clicks(), - binding.mangaInfoToggleLess.clicks(), - binding.mangaSummarySection.clicks(), - ) - .onEach { toggleMangaInfo() } - .launchIn(controller.viewScope) - - if (initialLoad) { - binding.mangaGenresTagsCompact.requestLayout() - } - - // Expand manga info if navigated from source listing or explicitly set to - // (e.g. on tablets) - if (initialLoad && (fromSource || isTablet)) { - toggleMangaInfo() - initialLoad = false - } - } - } - - private fun showMangaInfo(visible: Boolean) { - binding.mangaSummarySection.isVisible = visible - } - - private fun toggleMangaInfo() { - val isCurrentlyExpanded = binding.mangaSummaryText.maxLines != maxLines - - binding.mangaInfoToggleMore.isVisible = isCurrentlyExpanded - binding.mangaInfoScrim.isVisible = isCurrentlyExpanded - binding.mangaInfoToggleMoreScrim.isVisible = isCurrentlyExpanded - binding.mangaGenresTagsCompact.isVisible = isCurrentlyExpanded - binding.mangaGenresTagsCompactChips.isVisible = isCurrentlyExpanded - - binding.mangaInfoToggleLess.isVisible = !isCurrentlyExpanded - // SY --> binding.mangaGenresTagsFullChips.isVisible = !isCurrentlyExpanded - binding.genreGroups.isVisible = !isCurrentlyExpanded + binding.mangaSummarySection.isVisible = !manga.description.isNullOrBlank() || !manga.genre.isNullOrBlank() + binding.mangaSummarySection.description = manga.description + // SY --> + binding.mangaSummarySection.setTags( + manga.getGenres(), + meta, + controller::performGenreSearch, + controller::performGlobalSearch, + source + ) // SY <-- - - binding.mangaSummaryText.text = updateDescription(manga.description, isCurrentlyExpanded) - - binding.mangaSummaryText.maxLines = when { - isCurrentlyExpanded -> maxLines - else -> Int.MAX_VALUE - } } - private fun updateDescription(description: String?, isCurrentlyExpanded: Boolean): CharSequence { - return when { - description.isNullOrBlank() -> view.context.getString(R.string.unknown) - // SY --> - description == "meta" -> "" - // SY <-- - isCurrentlyExpanded -> - description - .replace(Regex(" +\$", setOf(RegexOption.MULTILINE)), "") - .replace(Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE)), "\n") - else -> description + /** + * Update favorite button with correct drawable and text. + * + * @param isFavorite determines if manga is favorite or not. + */ + private fun setFavoriteButtonState(isFavorite: Boolean) { + // Set the Favorite drawable to the correct one. + // Border drawable if false, filled drawable if true. + val (iconResource, stringResource) = when (isFavorite) { + true -> R.drawable.ic_favorite_24dp to R.string.in_library + false -> R.drawable.ic_favorite_border_24dp to R.string.add_to_library + } + binding.btnFavorite.apply { + setIconResource(iconResource) + text = context.getString(stringResource) + isActivated = isFavorite } } @@ -502,66 +406,6 @@ class MangaInfoHeaderAdapter( }.distinct().joinToString() } } - - private fun setChipsWithNamespace(genre: List?, meta: RaisedSearchMetadata?) { - val namespaceTags = when { - meta != null -> { - meta.tags - .filterNot { it.type == RaisedSearchMetadata.TAG_TYPE_VIRTUAL } - .groupBy { it.namespace } - .map { (namespace, tags) -> - NamespaceTagsItem( - namespace, - tags.map { - it.name to it.type - } - ) - } - } - genre != null -> { - if (genre.all { it.contains(':') }) { - genre - .map { tag -> - val index = tag.indexOf(':') - tag.substring(0, index).trim() to tag.substring(index + 1).trim() - } - .groupBy { - it.first - } - .mapValues { group -> - group.value.map { it.second to 0 } - } - .map { (namespace, tags) -> - NamespaceTagsItem(namespace, tags) - } - } else { - listOf(NamespaceTagsItem(null, genre.map { it to null })) - } - } - else -> emptyList() - } - - mangaTagsInfoAdapter?.updateDataSet(namespaceTags) - } // SY <-- - - /** - * Update favorite button with correct drawable and text. - * - * @param isFavorite determines if manga is favorite or not. - */ - private fun setFavoriteButtonState(isFavorite: Boolean) { - // Set the Favorite drawable to the correct one. - // Border drawable if false, filled drawable if true. - val (iconResource, stringResource) = when (isFavorite) { - true -> R.drawable.ic_favorite_24dp to R.string.in_library - false -> R.drawable.ic_favorite_border_24dp to R.string.add_to_library - } - binding.btnFavorite.apply { - setIconResource(iconResource) - text = context.getString(stringResource) - isActivated = isFavorite - } - } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/NamespaceTagsAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/NamespaceTagsAdapter.kt deleted file mode 100644 index 1fadc623a..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/NamespaceTagsAdapter.kt +++ /dev/null @@ -1,8 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga.info - -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.kanade.tachiyomi.source.Source -import eu.kanade.tachiyomi.ui.manga.MangaController - -class NamespaceTagsAdapter(val controller: MangaController, val source: Source) : - FlexibleAdapter(null, controller, true) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/NamespaceTagsHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/NamespaceTagsHolder.kt index 1d821ee4a..da7bbb78f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/NamespaceTagsHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/NamespaceTagsHolder.kt @@ -5,6 +5,7 @@ import android.widget.LinearLayout import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import com.google.android.material.chip.Chip +import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.viewholders.FlexibleViewHolder import eu.kanade.tachiyomi.databinding.MangaInfoGenreGroupingBinding import eu.kanade.tachiyomi.util.system.dpToPx @@ -12,7 +13,7 @@ import exh.util.makeSearchChip class NamespaceTagsHolder( view: View, - val adapter: NamespaceTagsAdapter + adapter: FlexibleAdapter<*> ) : FlexibleViewHolder(view, adapter) { val binding = MangaInfoGenreGroupingBinding.bind(view) @@ -40,9 +41,9 @@ class NamespaceTagsHolder( item.tags.map { (tag, type) -> binding.root.context.makeSearchChip( tag, - adapter.controller::performSearch, - adapter.controller::performGlobalSearch, - adapter.source.id, + item.onClick, + item.onLongClick, + item.source.id, namespace, type ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/NamespaceTagsItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/NamespaceTagsItem.kt index 3c32833af..a9083b499 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/NamespaceTagsItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/NamespaceTagsItem.kt @@ -6,8 +6,15 @@ import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import eu.davidea.flexibleadapter.items.IFlexible import eu.kanade.tachiyomi.R +import tachiyomi.source.Source -class NamespaceTagsItem(val namespace: String?, val tags: List>) : +class NamespaceTagsItem( + val namespace: String?, + val tags: List>, + val onClick: (item: String) -> Unit, + val onLongClick: (item: String) -> Unit, + val source: Source +) : AbstractFlexibleItem() { override fun getLayoutRes(): Int { @@ -15,7 +22,7 @@ class NamespaceTagsItem(val namespace: String?, val tags: List>): NamespaceTagsHolder { - return NamespaceTagsHolder(view, adapter as NamespaceTagsAdapter) + return NamespaceTagsHolder(view, adapter) } override fun bindViewHolder( diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/MangaSummaryView.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/MangaSummaryView.kt new file mode 100644 index 000000000..01e18ad3d --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/MangaSummaryView.kt @@ -0,0 +1,297 @@ +package eu.kanade.tachiyomi.widget + +import android.animation.AnimatorSet +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.drawable.Animatable +import android.util.AttributeSet +import android.view.LayoutInflater +import android.widget.FrameLayout +import androidx.annotation.AttrRes +import androidx.annotation.StyleRes +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.ContextCompat +import androidx.core.view.doOnNextLayout +import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams +import androidx.interpolator.view.animation.FastOutSlowInInterpolator +import androidx.recyclerview.widget.LinearLayoutManager +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.databinding.MangaSummaryBinding +import eu.kanade.tachiyomi.ui.manga.info.NamespaceTagsItem +import eu.kanade.tachiyomi.util.system.animatorDurationScale +import eu.kanade.tachiyomi.util.system.copyToClipboard +import exh.metadata.metadata.base.RaisedSearchMetadata +import exh.util.setChipsExtended +import tachiyomi.source.Source +import kotlin.math.roundToInt +import kotlin.math.roundToLong + +class MangaSummaryView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + @AttrRes defStyleAttr: Int = 0, + @StyleRes defStyleRes: Int = 0 +) : FrameLayout(context, attrs, defStyleAttr, defStyleRes) { + + private val binding = MangaSummaryBinding.inflate(LayoutInflater.from(context), this, true) + + private var animatorSet: AnimatorSet? = null + + private var recalculateHeights = false + private var descExpandedHeight = -1 + private var descShrunkHeight = -1 + + // SY --> + private var mangaTagsInfoAdapter = FlexibleAdapter(emptyList()) + // SY <-- + + var expanded = false + set(value) { + if (field != value) { + field = value + updateExpandState() + } + } + + var description: CharSequence? = null + set(value) { + if (field != value) { + field = if (value.isNullOrBlank()) { + context.getString(R.string.unknown) + // SY --> + } else if (value == "meta") { + "" + // SY <-- + } else { + value + } + binding.descriptionText.text = field + recalculateHeights = true + doOnNextLayout { + updateExpandState() + } + requestLayout() + } + } + + // SY --> + fun setTags( + items: List?, + meta: RaisedSearchMetadata?, + onClick: (item: String) -> Unit, + onLongClick: (item: String) -> Unit, + source: Source + ) { + binding.tagChipsShrunk.setChipsExtended( + items, + onClick, + onLongClick, + source.id + ) + // binding.tagChipsExpanded.setChips(items, onClick) + setChipsWithNamespace( + items, + meta, + onClick, + onLongClick, + source + ) + } + // SY <-- + + private fun updateExpandState() = binding.apply { + val initialSetup = descriptionText.maxHeight < 0 + + val maxHeightTarget = if (expanded) descExpandedHeight else descShrunkHeight + val maxHeightStart = if (initialSetup) maxHeightTarget else descriptionText.maxHeight + val descMaxHeightAnimator = ValueAnimator().apply { + setIntValues(maxHeightStart, maxHeightTarget) + addUpdateListener { + descriptionText.maxHeight = it.animatedValue as Int + } + } + + val toggleDrawable = ContextCompat.getDrawable( + context, + if (expanded) R.drawable.anim_caret_up else R.drawable.anim_caret_down + ) + toggleMore.setImageDrawable(toggleDrawable) + + var pastHalf = false + val toggleTarget = if (expanded) 1F else 0F + val toggleStart = if (initialSetup) { + toggleTarget + } else { + toggleMore.translationY / toggleMore.height + } + val toggleAnimator = ValueAnimator().apply { + setFloatValues(toggleStart, toggleTarget) + addUpdateListener { + val value = it.animatedValue as Float + + toggleMore.translationY = toggleMore.height * value + descriptionScrim.translationY = toggleMore.translationY + toggleMoreScrim.translationY = toggleMore.translationY + tagChipsShrunkContainer.updateLayoutParams { + topMargin = toggleMore.translationY.roundToInt() + } + tagChipsExpanded.updateLayoutParams { + topMargin = toggleMore.translationY.roundToInt() + } + + // Update non-animatable objects mid-animation makes it feel less abrupt + if (it.animatedFraction >= 0.5F && !pastHalf) { + pastHalf = true + descriptionText.text = trimWhenNeeded(description) + tagChipsShrunkContainer.scrollX = 0 + tagChipsShrunkContainer.isVisible = !expanded + tagChipsExpanded.isVisible = expanded + } + } + } + + animatorSet?.cancel() + animatorSet = AnimatorSet().apply { + interpolator = FastOutSlowInInterpolator() + duration = (TOGGLE_ANIM_DURATION * context.animatorDurationScale).roundToLong() + playTogether(toggleAnimator, descMaxHeightAnimator) + start() + } + (toggleDrawable as? Animatable)?.start() + } + + private fun trimWhenNeeded(text: CharSequence?): CharSequence? { + return if (!expanded) { + text + ?.replace(Regex(" +\$", setOf(RegexOption.MULTILINE)), "") + ?.replace(Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE)), "\n") + } else { + text + } + } + + // SY --> + private fun setChipsWithNamespace( + genre: List?, + meta: RaisedSearchMetadata?, + onClick: (item: String) -> Unit, + onLongClick: (item: String) -> Unit, + source: Source + ) { + val namespaceTags = when { + meta != null -> { + meta.tags + .filterNot { it.type == RaisedSearchMetadata.TAG_TYPE_VIRTUAL } + .groupBy { it.namespace } + .map { (namespace, tags) -> + NamespaceTagsItem( + namespace, + tags.map { + it.name to it.type + }, + onClick, + onLongClick, + source + ) + } + } + genre != null -> { + if (genre.all { it.contains(':') }) { + genre + .map { tag -> + val index = tag.indexOf(':') + tag.substring(0, index).trim() to tag.substring(index + 1).trim() + } + .groupBy { + it.first + } + .mapValues { group -> + group.value.map { it.second to 0 } + } + .map { (namespace, tags) -> + NamespaceTagsItem( + namespace, + tags, + onClick, + onLongClick, + source + ) + } + } else { + listOf( + NamespaceTagsItem( + null, + genre.map { it to null }, + onClick, + onLongClick, + source + ) + ) + } + } + else -> emptyList() + } + + mangaTagsInfoAdapter.updateDataSet(namespaceTags) + } + // SY <-- + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + if (!recalculateHeights) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + return + } + recalculateHeights = false + + // Measure with expanded lines + binding.descriptionText.maxLines = Int.MAX_VALUE + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + descExpandedHeight = binding.descriptionText.measuredHeight + + // Measure with shrunk lines + binding.descriptionText.maxLines = SHRUNK_DESC_MAX_LINES + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + descShrunkHeight = binding.descriptionText.measuredHeight + } + + init { + binding.descriptionText.apply { + // So that 1 line of text won't be hidden by scrim + minLines = DESC_MIN_LINES + + setOnLongClickListener { + context.copyToClipboard( + context.getString(R.string.description), + text.toString() + ) + true + } + } + + // SY --> + binding.tagChipsExpanded.layoutManager = LinearLayoutManager(binding.root.context) + binding.tagChipsExpanded.adapter = mangaTagsInfoAdapter + + mangaTagsInfoAdapter.mItemClickListener = FlexibleAdapter.OnItemClickListener { _, _ -> + expanded = !expanded + false + } + // SY <-- + + arrayOf( + binding.descriptionText, + binding.descriptionScrim, + binding.toggleMoreScrim, + binding.toggleMore + ).forEach { + it.setOnClickListener { expanded = !expanded } + } + } +} + +private const val TOGGLE_ANIM_DURATION = 300L + +private const val DESC_MIN_LINES = 2 +private const val SHRUNK_DESC_MAX_LINES = 3 diff --git a/app/src/main/res/drawable/anim_caret_down.xml b/app/src/main/res/drawable/anim_caret_down.xml new file mode 100644 index 000000000..e5288406e --- /dev/null +++ b/app/src/main/res/drawable/anim_caret_down.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/anim_caret_up.xml b/app/src/main/res/drawable/anim_caret_up.xml new file mode 100644 index 000000000..78b817a16 --- /dev/null +++ b/app/src/main/res/drawable/anim_caret_up.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout-sw720dp/manga_info_header.xml b/app/src/main/res/layout-sw720dp/manga_info_header.xml index f74940904..5165c6928 100644 --- a/app/src/main/res/layout-sw720dp/manga_info_header.xml +++ b/app/src/main/res/layout-sw720dp/manga_info_header.xml @@ -209,141 +209,12 @@ app:layout_constraintTop_toBottomOf="@+id/manga_actions" tools:visibility="gone" /> - - - - - - - - - - - - - - - - - - - - - - - - + app:layout_constraintTop_toBottomOf="@id/metadata_view" /> diff --git a/app/src/main/res/layout/manga_info_header.xml b/app/src/main/res/layout/manga_info_header.xml index c481b1ce0..5121a86e7 100644 --- a/app/src/main/res/layout/manga_info_header.xml +++ b/app/src/main/res/layout/manga_info_header.xml @@ -221,139 +221,12 @@ app:layout_constraintTop_toBottomOf="@+id/manga_actions" tools:visibility="gone" /> - - - - - - - - - - - - - - - - - - - - - - - + app:layout_constraintTop_toBottomOf="@id/metadata_view" /> diff --git a/app/src/main/res/layout/manga_summary.xml b/app/src/main/res/layout/manga_summary.xml new file mode 100644 index 000000000..0533ffdc0 --- /dev/null +++ b/app/src/main/res/layout/manga_summary.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + +