diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt index 5534e68f3..8217d440f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt @@ -2,15 +2,10 @@ package eu.kanade.tachiyomi.ui.manga import android.animation.Animator import android.animation.AnimatorListenerAdapter -import android.animation.AnimatorSet -import android.animation.ObjectAnimator import android.app.Activity import android.content.Context import android.content.Intent import android.graphics.Bitmap -import android.graphics.Point -import android.graphics.Rect -import android.graphics.RectF import android.graphics.drawable.BitmapDrawable import android.os.Bundle import android.view.LayoutInflater @@ -18,8 +13,6 @@ import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import android.view.View -import android.view.animation.DecelerateInterpolator -import android.widget.ImageView import android.widget.TextView import androidx.annotation.FloatRange import androidx.appcompat.app.AppCompatActivity @@ -31,7 +24,6 @@ import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import coil.imageLoader -import coil.loadAny import coil.request.ImageRequest import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeType @@ -85,7 +77,6 @@ import eu.kanade.tachiyomi.ui.manga.chapter.MangaChaptersHeaderAdapter import eu.kanade.tachiyomi.ui.manga.chapter.base.BaseChaptersAdapter import eu.kanade.tachiyomi.ui.manga.info.MangaInfoButtonsAdapter import eu.kanade.tachiyomi.ui.manga.info.MangaInfoHeaderAdapter -import eu.kanade.tachiyomi.ui.manga.info.MangaInfoItemAdapter import eu.kanade.tachiyomi.ui.manga.merged.EditMergedSettingsDialog import eu.kanade.tachiyomi.ui.manga.track.TrackItem import eu.kanade.tachiyomi.ui.manga.track.TrackSearchDialog @@ -107,6 +98,7 @@ import eu.kanade.tachiyomi.util.view.snack import exh.log.xLogD import exh.md.similar.MangaDexSimilarController import exh.metadata.metadata.base.FlatMetadata +import exh.metadata.metadata.base.RaisedSearchMetadata import exh.recs.RecommendsController import exh.source.MERGED_SOURCE_ID import exh.source.getMainSource @@ -117,7 +109,6 @@ import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.withContext -import reactivecircus.flowbinding.android.view.clicks import reactivecircus.flowbinding.recyclerview.scrollEvents import reactivecircus.flowbinding.swiperefreshlayout.refreshes import timber.log.Timber @@ -192,15 +183,13 @@ class MangaController : private var mangaInfoAdapter: MangaInfoHeaderAdapter? = null - // SY >-- - private var mangaInfoItemAdapter: MangaInfoItemAdapter? = null - private var mangaInfoButtonsAdapter: MangaInfoButtonsAdapter? = null - private var mangaMetaInfoAdapter: RecyclerView.Adapter<*>? = null - - // SY <-- private var chaptersHeaderAdapter: MangaChaptersHeaderAdapter? = null private var chaptersAdapter: ChaptersAdapter? = null + // SY --> + private var mangaInfoButtonsAdapter: MangaInfoButtonsAdapter? = null + // SY <-- + // Sheet containing filter/sort/display items. private var settingsSheet: ChaptersSettingsSheet? = null @@ -237,8 +226,6 @@ class MangaController : private var editMangaDialog: EditMangaDialog? = null private var editMergedSettingsDialog: EditMergedSettingsDialog? = null - - private var currentAnimator: Animator? = null // EXH <-- init { @@ -298,18 +285,13 @@ class MangaController : if (manga == null || source == null) return // Init RecyclerView and adapter - mangaInfoAdapter = MangaInfoHeaderAdapter(this, binding.infoRecycler != null) + mangaInfoAdapter = MangaInfoHeaderAdapter(this, fromSource, binding.infoRecycler != null) chaptersHeaderAdapter = MangaChaptersHeaderAdapter(this) chaptersAdapter = ChaptersAdapter(this, view.context) // SY --> - val mainSource = presenter.source.getMainSource>() - if (mainSource != null) { - mangaMetaInfoAdapter = mainSource.getDescriptionAdapter(this) - } if (!preferences.recommendsInOverflow().get() || smartSearchConfig != null) { mangaInfoButtonsAdapter = MangaInfoButtonsAdapter(this) } - mangaInfoItemAdapter = MangaInfoItemAdapter(this, fromSource, binding.infoRecycler != null) // SY <-- // Phone layout @@ -317,8 +299,6 @@ class MangaController : it.adapter = ConcatAdapter( listOfNotNull( mangaInfoAdapter, - mangaMetaInfoAdapter, - mangaInfoItemAdapter, mangaInfoButtonsAdapter, chaptersHeaderAdapter, chaptersAdapter @@ -346,9 +326,7 @@ class MangaController : it.adapter = ConcatAdapter( listOfNotNull( mangaInfoAdapter, - mangaInfoButtonsAdapter, - mangaMetaInfoAdapter, - mangaInfoItemAdapter + mangaInfoButtonsAdapter ) ) @@ -459,11 +437,6 @@ class MangaController : chaptersHeaderAdapter = null chaptersAdapter = null settingsSheet = null - // SY --> - mangaInfoButtonsAdapter = null - mangaInfoItemAdapter = null - mangaMetaInfoAdapter = null - // SY <-- addSnackbar?.dismiss() updateToolbarTitleAlpha(1F) toolbarTextView = null @@ -559,7 +532,7 @@ class MangaController : val mainSource = presenter.source.getMainSource>() if (mainSource != null) { presenter.meta = flatMetadata.raise(mainSource.metaClass) - mangaMetaInfoAdapter?.notifyDataSetChanged() + mangaInfoAdapter?.notifyMetaAdapter() updateFilterIconState() } } @@ -573,13 +546,10 @@ class MangaController : * @param manga manga object containing information about manga. * @param source the source of the manga. */ - fun onNextMangaInfo(manga: Manga, source: Source) { + fun onNextMangaInfo(manga: Manga, source: Source, metadata: RaisedSearchMetadata?) { if (manga.initialized) { // Update view. - mangaInfoAdapter?.update(manga, source) - mangaInfoItemAdapter?.update(manga, source, presenter.meta) - - binding.expandedImage.loadAny(manga) + mangaInfoAdapter?.update(manga, source, metadata, presenter.mergedMangaReferences) } else { // Initialize manga. fetchMangaInfoFromSource() @@ -843,110 +813,6 @@ class MangaController : presenter.moveMangaToCategories(manga, categories) } - // SY --> - fun onThumbnailClick(thumbView: ImageView) { - if (!presenter.manga.initialized || presenter.manga.thumbnail_url == null) return - currentAnimator?.cancel() - - val startBoundsInt = Rect() - val finalBoundsInt = Rect() - val globalOffset = Point() - - thumbView.getGlobalVisibleRect(startBoundsInt) - binding.root.getGlobalVisibleRect(finalBoundsInt, globalOffset) - startBoundsInt.offset(-globalOffset.x, -globalOffset.y) - finalBoundsInt.offset(-globalOffset.x, -globalOffset.y) - - val startBounds = RectF(startBoundsInt) - val finalBounds = RectF(finalBoundsInt) - - val startScale: Float - if ((finalBounds.width() / finalBounds.height() > startBounds.width() / startBounds.height())) { - startScale = startBounds.height() / finalBounds.height() - val startWidth: Float = startScale * finalBounds.width() - val deltaWidth: Float = (startWidth - startBounds.width()) / 2 - startBounds.left -= deltaWidth.toInt() - startBounds.right += deltaWidth.toInt() - } else { - startScale = startBounds.width() / finalBounds.width() - val startHeight: Float = startScale * finalBounds.height() - val deltaHeight: Float = (startHeight - startBounds.height()) / 2f - startBounds.top -= deltaHeight.toInt() - startBounds.bottom += deltaHeight.toInt() - } - thumbView.alpha = 0f - actionFab?.isVisible = false - binding.expandedImage.isVisible = true - - binding.expandedImage.pivotX = 0f - binding.expandedImage.pivotY = 0f - - currentAnimator = AnimatorSet().apply { - play( - ObjectAnimator.ofFloat( - binding.expandedImage, - View.X, - startBounds.left, - finalBounds.left - ) - ).apply { - with(ObjectAnimator.ofFloat(binding.expandedImage, View.Y, startBounds.top, finalBounds.top)) - with(ObjectAnimator.ofFloat(binding.expandedImage, View.SCALE_X, startScale, 1f)) - with(ObjectAnimator.ofFloat(binding.expandedImage, View.SCALE_Y, startScale, 1f)) - } - duration = resources?.getInteger(android.R.integer.config_shortAnimTime)?.toLong() ?: 150L - interpolator = DecelerateInterpolator() - addListener( - object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - currentAnimator = null - } - - override fun onAnimationCancel(animation: Animator) { - currentAnimator = null - } - } - ) - start() - } - - binding.expandedImage.clicks() - .onEach { - currentAnimator?.cancel() - - currentAnimator = AnimatorSet().apply { - play(ObjectAnimator.ofFloat(binding.expandedImage, View.X, startBounds.left)).apply { - with(ObjectAnimator.ofFloat(binding.expandedImage, View.Y, startBounds.top)) - with(ObjectAnimator.ofFloat(binding.expandedImage, View.SCALE_X, startScale)) - with(ObjectAnimator.ofFloat(binding.expandedImage, View.SCALE_Y, startScale)) - } - duration = resources?.getInteger(android.R.integer.config_shortAnimTime)?.toLong() ?: 150L - interpolator = DecelerateInterpolator() - addListener( - object : AnimatorListenerAdapter() { - - override fun onAnimationEnd(animation: Animator) { - thumbView.alpha = 1f - binding.expandedImage.isVisible = false - actionFab?.isVisible = presenter.filteredAndSortedChapters.any { !it.read } - currentAnimator = null - } - - override fun onAnimationCancel(animation: Animator) { - thumbView.alpha = 1f - binding.expandedImage.isVisible = false - actionFab?.isVisible = presenter.filteredAndSortedChapters.any { !it.read } - currentAnimator = null - } - } - ) - start() - } - } - .launchIn(viewScope) - } - // SY <-- - /** * Perform a global search using the provided query. * diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt index a84ea85df..b812a7508 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt @@ -154,6 +154,8 @@ class MangaPresenter( var mergedManga = emptyMap() private set + var mergedMangaReferences = emptyList() + private set var dedupe: Boolean = true @@ -166,6 +168,7 @@ class MangaPresenter( // SY --> if (source is MergedSource) { launchIO { mergedManga = db.getMergedMangas(manga.id!!).executeAsBlocking().associateBy { it.id!! } } + launchIO { mergedMangaReferences = db.getMergedMangaReferences(manga.id!!).executeAsBlocking() } } // SY <-- @@ -188,11 +191,13 @@ class MangaPresenter( } } .subscribeLatestCache({ view, (manga, flatMetadata) -> - flatMetadata?.let { metadata -> - view.onNextMetaInfo(metadata) - } + val mainSource = source.getMainSource>() + val meta = if (mainSource != null) { + flatMetadata?.raise(mainSource.metaClass) + } else null + this.meta = meta // SY <-- - view.onNextMangaInfo(manga, source) + view.onNextMangaInfo(manga, source, meta) }) getTrackingObservable() @@ -383,7 +388,7 @@ class MangaPresenter( .observeOn(AndroidSchedulers.mainThread()) .subscribeLatestCache( { view, _ -> - view.onNextMangaInfo(manga, source) + view.onNextMangaInfo(manga, source, meta) } ) } 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 e56e54b5f..ba6b44605 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 @@ -4,11 +4,13 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible +import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import coil.loadAny +import coil.target.ImageViewTarget import com.google.android.material.dialog.MaterialAlertDialogBuilder +import eu.davidea.flexibleadapter.FlexibleAdapter import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.databinding.MangaInfoHeaderBinding @@ -16,38 +18,48 @@ import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.HttpSource -import eu.kanade.tachiyomi.source.online.all.MergedSource +import eu.kanade.tachiyomi.source.online.MetadataSource 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.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 -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy class MangaInfoHeaderAdapter( private val controller: MangaController, - private val isTablet: Boolean + private val fromSource: Boolean, + private val isTablet: Boolean, ) : RecyclerView.Adapter() { private val trackManager: TrackManager by injectLazy() // SY --> - private val db: DatabaseHelper by injectLazy() private val sourceManager: SourceManager by injectLazy() - private var mergedMangaReferences: List = emptyList() // SY <-- private var manga: Manga = controller.presenter.manga private var source: Source = controller.presenter.source + + // SY --> + private var meta: RaisedSearchMetadata? = controller.presenter.meta + private var mergedMangaReferences: List = controller.presenter.mergedMangaReferences + + // SY <-- private var trackCount: Int = 0 + private var metaInfoAdapter: RecyclerView.Adapter<*>? = null + private var mangaTagsInfoAdapter: NamespaceTagsAdapter = NamespaceTagsAdapter(controller, source) private lateinit var binding: MangaInfoHeaderBinding @@ -55,6 +67,20 @@ class MangaInfoHeaderAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeaderViewHolder { binding = MangaInfoHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false) + // SY --> + metaInfoAdapter = source.getMainSource>()?.getDescriptionAdapter(controller) + binding.metadataView.isVisible = if (metaInfoAdapter != null) { + binding.metadataView.layoutManager = LinearLayoutManager(binding.root.context) + binding.metadataView.adapter = metaInfoAdapter + true + } else { + false + } + + binding.genreGroups.layoutManager = LinearLayoutManager(binding.root.context) + binding.genreGroups.adapter = mangaTagsInfoAdapter + // SY <-- + return HeaderViewHolder(binding.root) } @@ -70,16 +96,16 @@ class MangaInfoHeaderAdapter( * @param manga manga object containing information about manga. * @param source the source of the manga. */ - fun update(manga: Manga, source: Source) { + fun update(manga: Manga, source: Source, meta: RaisedSearchMetadata?, mergedMangaReferences: List) { this.manga = manga this.source = source // SY --> - if (source is MergedSource) { - mergedMangaReferences = db.getMergedMangaReferences(manga.id!!).executeAsBlocking() - } + this.meta = meta + this.mergedMangaReferences = mergedMangaReferences // SY <-- notifyDataSetChanged() + notifyMetaAdapter() } fun setTrackingCount(trackCount: Int) { @@ -88,14 +114,22 @@ class MangaInfoHeaderAdapter( notifyDataSetChanged() } + fun notifyMetaAdapter() { + metaInfoAdapter?.notifyDataSetChanged() + } + inner class HeaderViewHolder(private val view: View) : RecyclerView.ViewHolder(view) { fun bind() { // For rounded corners binding.mangaCover.clipToOutline = true + // SY --> - binding.mangaCover.clicks() - .onEach { controller.onThumbnailClick(binding.mangaCover) } - .launchIn(controller.viewScope) + mangaTagsInfoAdapter.mItemClickListener = FlexibleAdapter.OnItemClickListener { _, _ -> + controller.viewScope.launchUI { + toggleMangaInfo() + } + false + } // SY <-- binding.btnFavorite.clicks() @@ -209,13 +243,22 @@ 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.longClicks() .onEach { showCoverOptionsDialog() } .launchIn(controller.viewScope) - setMangaInfo(manga, source) + setMangaInfo(manga, source, meta) } private fun showCoverOptionsDialog() { @@ -245,7 +288,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?) { + private fun setMangaInfo(manga: Manga, source: Source?, meta: RaisedSearchMetadata?) { // Update full title TextView. binding.mangaFullTitle.text = if (manga.title.isBlank()) { view.context.getString(R.string.unknown) @@ -278,7 +321,6 @@ class MangaInfoHeaderAdapter( } else /* SY <-- */ if (mangaSource != null) { text = mangaSource setOnClickListener { - val sourceManager = Injekt.get() controller.performSearch(sourceManager.getOrStub(source.id).name) } } else { @@ -305,16 +347,141 @@ class MangaInfoHeaderAdapter( setFavoriteButtonState(manga.favorite) // Set cover if changed. - listOf(binding.mangaCover, binding.backdrop).forEach { - it.loadAny(manga) + binding.backdrop.loadAny(manga) + binding.mangaCover.loadAny(manga) { + listener( + onSuccess = { request, _ -> + (request.target as? ImageViewTarget)?.drawable?.let { drawable -> + val ratio = drawable.minimumWidth / drawable.minimumHeight.toFloat() + binding.root.getConstraintSet(R.id.end) + ?.setDimensionRatio(R.id.manga_cover, ratio.toString()) + } + } + ) } - if (initialLoad && isTablet) { - initialLoad = false - // wrap_content and autoFixTextSize can cause unwanted behaviour this tries to solve it - binding.mangaFullTitle.requestLayout() + + // Manga info section + val hasInfoContent = !manga.description.isNullOrBlank() || !manga.genre.isNullOrBlank() + showMangaInfo(hasInfoContent) + if (hasInfoContent) { + // Update description TextView. + binding.mangaSummaryText.text = if (manga.description.isNullOrBlank()) { + view.context.getString(R.string.unknown) + } else { + manga.description + } + + // SY --> + if (manga.description == "meta") { + binding.mangaSummaryText.text = "" + /*binding.mangaInfoToggleLess.updateLayoutParams { + topToBottom = -1 + bottomToBottom = binding.mangaSummaryText.id + }*/ + } + // SY <-- + + // Update genres list + if (!manga.genre.isNullOrBlank()) { + binding.mangaGenresTagsCompactChips.setChips( + manga.getGenres(), + controller::performGenreSearch + ) + // SY --> + // if (source?.getMainSource() != null) { + setChipsWithNamespace( + manga.genre, + meta + ) + // binding.mangaGenresTagsFullChips.isVisible = false + /*} else { + binding.mangaGenresTagsFullChips.setChips( + manga.getGenres(), + controller::performGenreSearch + ) + binding.genreGroups.isVisible = false + }*/ + // SY <-- + } else { + 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) + + // Expand manga info if navigated from source listing or explicitly set to + // (e.g. on tablets) + if (initialLoad && (fromSource || isTablet)) { + toggleMangaInfo() + initialLoad = false + // wrap_content and autoFixTextSize can cause unwanted behaviour this tries to solve it + binding.mangaFullTitle.requestLayout() + } + + // Refreshes will change the state and it needs to be set to correct state to display correctly + if (binding.mangaSummaryText.maxLines == 2) { + binding.mangaSummarySection.transitionToState(R.id.start) + } else { + binding.mangaSummarySection.transitionToState(R.id.end) + } } } + private fun showMangaInfo(visible: Boolean) { + binding.mangaSummarySection.isVisible = visible + } + + private fun toggleMangaInfo() { + val isCurrentlyExpanded = binding.mangaSummaryText.maxLines != 2 + + if (isCurrentlyExpanded) { + binding.mangaSummarySection.transitionToStart() + } else { + binding.mangaSummarySection.transitionToEnd() + } + + binding.mangaSummaryText.maxLines = if (isCurrentlyExpanded) { + 2 + } else { + Int.MAX_VALUE + } + } + + private fun setChipsWithNamespace(genre: String?, 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 -> { + listOf(NamespaceTagsItem(null, genre.split(",").map { it.trim() to null })) + } + else -> emptyList() + } + + mangaTagsInfoAdapter.updateDataSet(namespaceTags) + } + /** * Update favorite button with correct drawable and text. * diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoItemAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoItemAdapter.kt deleted file mode 100644 index 376ce4560..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoItemAdapter.kt +++ /dev/null @@ -1,229 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga.info - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.core.view.isVisible -import androidx.core.view.updateLayoutParams -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.IFlexible -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.databinding.MangaInfoItemBinding -import eu.kanade.tachiyomi.source.Source -import eu.kanade.tachiyomi.source.online.NamespaceSource -import eu.kanade.tachiyomi.ui.manga.MangaController -import eu.kanade.tachiyomi.util.system.copyToClipboard -import exh.metadata.metadata.base.RaisedSearchMetadata -import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL -import exh.source.getMainSource -import exh.source.isEhBasedSource -import exh.util.getRaisedTags -import exh.util.makeSearchChip -import exh.util.setChipsExtended -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.merge -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import reactivecircus.flowbinding.android.view.clicks -import reactivecircus.flowbinding.android.view.longClicks - -class MangaInfoItemAdapter( - private val controller: MangaController, - private val fromSource: Boolean, - private val isTablet: Boolean -) : - RecyclerView.Adapter() { - - private var manga: Manga = controller.presenter.manga - private var source: Source = controller.presenter.source - private var meta: RaisedSearchMetadata? = controller.presenter.meta - - private lateinit var binding: MangaInfoItemBinding - - private var initialLoad: Boolean = true - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeaderViewHolder { - binding = MangaInfoItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) - return HeaderViewHolder(binding.root) - } - - private var mangaTagsInfoAdapter: FlexibleAdapter>? = FlexibleAdapter(null) - - override fun getItemCount(): Int = 1 - - override fun onBindViewHolder(holder: HeaderViewHolder, position: Int) { - holder.bind() - } - - /** - * Update the view with manga information. - * - * @param manga manga object containing information about manga. - * @param source the source of the manga. - */ - fun update(manga: Manga, source: Source, meta: RaisedSearchMetadata?) { - this.manga = manga - this.source = source - this.meta = meta - - notifyDataSetChanged() - } - - inner class HeaderViewHolder(private val view: View) : RecyclerView.ViewHolder(view) { - fun bind() { - binding.mangaSummaryText.longClicks() - .onEach { - controller.activity?.copyToClipboard( - view.context.getString(R.string.description), - binding.mangaSummaryText.text.toString() - ) - } - .launchIn(controller.viewScope) - - binding.genreGroups.layoutManager = LinearLayoutManager(itemView.context) - binding.genreGroups.adapter = mangaTagsInfoAdapter - - // SY --> - mangaTagsInfoAdapter?.mItemClickListener = FlexibleAdapter.OnItemClickListener { _, _ -> - controller.viewScope.launch { - toggleMangaInfo() - } - false - } - // SY <-- - - setMangaInfo(manga, source) - } - - /** - * Update the view with manga information. - * - * @param manga manga object containing information about manga. - * @param source the source of the manga. - */ - private fun setMangaInfo(manga: Manga, source: Source?) { - // Manga info section - val hasInfoContent = !manga.description.isNullOrBlank() || !manga.genre.isNullOrBlank() - showMangaInfo(hasInfoContent) - if (hasInfoContent) { - // Update description TextView. - binding.mangaSummaryText.text = if (manga.description.isNullOrBlank()) { - view.context.getString(R.string.unknown) - } else { - manga.description - } - - // SY --> - if (binding.mangaSummaryText.text == "meta") { - binding.mangaSummaryText.text = "" - binding.mangaSummaryText.maxLines = 1 - binding.mangaInfoToggleLess.updateLayoutParams { - topToBottom = -1 - bottomToBottom = binding.mangaSummaryText.id - } - } - // SY <-- - - // Update genres list - if (!manga.genre.isNullOrBlank()) { - // SY --> - if (source != null && source.getMainSource() is NamespaceSource) { - val metaTags = meta?.tags?.filterNot { it.type == TAG_TYPE_VIRTUAL }?.groupBy { it.namespace } - var namespaceTags: List = emptyList() - if (source.isEhBasedSource() && metaTags != null && metaTags.all { it.key != null }) { - namespaceTags = metaTags - .mapValues { values -> - values.value.map { - itemView.context.makeSearchChip( - it.name, - controller::performSearch, - controller::performGlobalSearch, - source.id, - it.namespace, - it.type - ) - } - } - .map { NamespaceTagsItem(it.key!!, it.value) } - } else { - val genre = manga.getRaisedTags() - if (!genre.isNullOrEmpty()) { - namespaceTags = genre - .groupBy { it.namespace } - .mapValues { values -> - values.value.map { - itemView.context.makeSearchChip( - it.name, - controller::performSearch, - controller::performGlobalSearch, - source.id, - it.namespace - ) - } - } - .map { NamespaceTagsItem(it.key, it.value) } - } - } - mangaTagsInfoAdapter?.updateDataSet(namespaceTags) - } - binding.mangaGenresTagsFullChips.setChipsExtended(manga.getGenres(), controller::performGenreSearch, controller::performGlobalSearch, source?.id ?: 0) - binding.mangaGenresTagsCompactChips.setChipsExtended(manga.getGenres(), controller::performGenreSearch, controller::performGlobalSearch, source?.id ?: 0) - // SY <-- - } else { - binding.mangaGenresTagsCompactChips.isVisible = false - binding.mangaGenresTagsFullChips.isVisible = false - // SY --> - binding.genreGroups.isVisible = false - // SY <-- - } - - // Handle showing more or less info - merge( - binding.mangaSummarySection.clicks(), - binding.mangaSummaryText.clicks(), - binding.mangaInfoToggleMore.clicks(), - binding.mangaInfoToggleLess.clicks() - ) - .onEach { toggleMangaInfo() } - .launchIn(controller.viewScope) - - // Expand manga info if navigated from source listing - if (initialLoad && (fromSource || isTablet)) { - toggleMangaInfo() - initialLoad = false - } - } - } - - private fun showMangaInfo(visible: Boolean) { - binding.mangaSummarySection.isVisible = visible - } - - private fun toggleMangaInfo() { - val isCurrentlyExpanded = binding.mangaSummaryText.maxLines != 2 /* SY --> */ && binding.mangaSummaryText.maxLines != 1 /* SY <-- */ - - binding.mangaInfoToggleMoreScrim.isVisible = isCurrentlyExpanded - binding.mangaInfoToggleMore.isVisible = isCurrentlyExpanded - binding.mangaInfoToggleLess.isVisible = !isCurrentlyExpanded - - binding.mangaSummaryText.maxLines = if (isCurrentlyExpanded) { - /* SY --> */ if (binding.mangaSummaryText.text.isBlank()) 1 else /* SY <-- */ 2 - } else { - Int.MAX_VALUE - } - - binding.mangaGenresTagsCompact.isVisible = isCurrentlyExpanded - // SY --> - if (source.getMainSource() !is NamespaceSource) { - binding.mangaGenresTagsFullChips.isVisible = !isCurrentlyExpanded - } else { - binding.genreGroups.isVisible = !isCurrentlyExpanded - } - // SY <-- - } - } -} 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 new file mode 100644 index 000000000..1fadc623a --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/NamespaceTagsAdapter.kt @@ -0,0 +1,8 @@ +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 new file mode 100644 index 000000000..9068c96de --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/NamespaceTagsHolder.kt @@ -0,0 +1,40 @@ +package eu.kanade.tachiyomi.ui.manga.info + +import android.view.View +import com.google.android.material.chip.Chip +import eu.davidea.viewholders.FlexibleViewHolder +import eu.kanade.tachiyomi.databinding.MangaInfoGenreGroupingBinding +import exh.util.makeSearchChip + +class NamespaceTagsHolder( + view: View, + val adapter: NamespaceTagsAdapter +) : FlexibleViewHolder(view, adapter) { + val binding = MangaInfoGenreGroupingBinding.bind(view) + + fun bind(item: NamespaceTagsItem) { + binding.namespace.removeAllViews() + val namespace = item.namespace + if (namespace != null) { + binding.namespace.addView( + Chip(binding.root.context).apply { + text = namespace + } + ) + } + + binding.tags.removeAllViews() + item.tags.map { (tag, type) -> + binding.root.context.makeSearchChip( + tag, + adapter.controller::performSearch, + adapter.controller::performGlobalSearch, + adapter.source.id, + namespace, + type + ) + }.forEach { + binding.tags.addView(it) + } + } +} 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 21cca007a..3c32833af 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 @@ -2,46 +2,43 @@ package eu.kanade.tachiyomi.ui.manga.info import android.view.View import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.chip.Chip -import com.google.android.material.chip.ChipGroup import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import eu.davidea.flexibleadapter.items.IFlexible -import eu.davidea.viewholders.FlexibleViewHolder import eu.kanade.tachiyomi.R -open class NamespaceTagsItem(val namespace: String?, val tags: List) : AbstractFlexibleItem() { +class NamespaceTagsItem(val namespace: String?, val tags: List>) : + AbstractFlexibleItem() { override fun getLayoutRes(): Int { return R.layout.manga_info_genre_grouping } - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): Holder { - return Holder(view, adapter) + override fun createViewHolder(view: View, adapter: FlexibleAdapter>): NamespaceTagsHolder { + return NamespaceTagsHolder(view, adapter as NamespaceTagsAdapter) } - override fun bindViewHolder(adapter: FlexibleAdapter>, holder: Holder, position: Int, payloads: List?) { - val namespaceChip = Chip(holder.itemView.context) - namespaceChip.text = namespace ?: holder.itemView.context.getString(R.string.unknown) - holder.namespaceChipGroup.addView(namespaceChip) - - tags.forEach { - holder.tagsChipGroup.addView(it) - } + override fun bindViewHolder( + adapter: FlexibleAdapter>, + holder: NamespaceTagsHolder, + position: Int, + payloads: List? + ) { + holder.bind(this) } override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false - return namespace == (other as NamespaceTagsItem).namespace + + other as NamespaceTagsItem + + if (namespace != other.namespace) return false + + return true } override fun hashCode(): Int { return namespace.hashCode() } - - class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) { - val namespaceChipGroup: ChipGroup = itemView.findViewById(R.id.namespace) - val tagsChipGroup: ChipGroup = itemView.findViewById(R.id.tags) - } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/lang/DateExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/lang/DateExtensions.kt index 521517b0f..c39cdd9f0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/lang/DateExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/lang/DateExtensions.kt @@ -122,5 +122,3 @@ fun Date.toRelativeString( else -> dateFormat.format(this) } } - - diff --git a/app/src/main/java/exh/util/SourceTagsUtil.kt b/app/src/main/java/exh/util/SourceTagsUtil.kt index 8bbfb8631..a5590efd9 100644 --- a/app/src/main/java/exh/util/SourceTagsUtil.kt +++ b/app/src/main/java/exh/util/SourceTagsUtil.kt @@ -12,7 +12,12 @@ import exh.source.nHentaiSourceIds import java.util.Locale object SourceTagsUtil { - fun getWrappedTag(sourceId: Long, namespace: String? = null, tag: String? = null, fullTag: String? = null): String? { + fun getWrappedTag( + sourceId: Long?, + namespace: String? = null, + tag: String? = null, + fullTag: String? = null + ): String? { return if (sourceId == EXH_SOURCE_ID || sourceId == EH_SOURCE_ID || sourceId in nHentaiSourceIds || sourceId in hitomiSourceIds) { val parsed = when { fullTag != null -> parseTag(fullTag) diff --git a/app/src/main/java/exh/util/ViewExtensions.kt b/app/src/main/java/exh/util/ViewExtensions.kt index 507231960..aa0c52ba4 100644 --- a/app/src/main/java/exh/util/ViewExtensions.kt +++ b/app/src/main/java/exh/util/ViewExtensions.kt @@ -24,7 +24,14 @@ fun ChipGroup.setChipsExtended(items: List?, onClick: (item: String) -> } } -fun Context.makeSearchChip(item: String, onClick: (item: String) -> Unit = {}, onLongClick: (item: String) -> Unit = {}, sourceId: Long, namespace: String? = null, type: Int? = null): Chip { +fun Context.makeSearchChip( + item: String, + onClick: (item: String) -> Unit = {}, + onLongClick: (item: String) -> Unit = {}, + sourceId: Long, + namespace: String? = null, + type: Int? = null +): Chip { return Chip(this).apply { text = item val search = if (namespace != null) { diff --git a/app/src/main/res/layout-sw720dp/manga_controller.xml b/app/src/main/res/layout-sw720dp/manga_controller.xml index 5f4fb7481..3a727142b 100644 --- a/app/src/main/res/layout-sw720dp/manga_controller.xml +++ b/app/src/main/res/layout-sw720dp/manga_controller.xml @@ -74,10 +74,4 @@ app:fastScrollerBubbleEnabled="false" tools:visibility="visible" /> - - diff --git a/app/src/main/res/layout-sw720dp/manga_info_header.xml b/app/src/main/res/layout-sw720dp/manga_info_header.xml new file mode 100644 index 000000000..61573829e --- /dev/null +++ b/app/src/main/res/layout-sw720dp/manga_info_header.xml @@ -0,0 +1,317 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/manga_controller.xml b/app/src/main/res/layout/manga_controller.xml index 0b7bead52..89d945a96 100755 --- a/app/src/main/res/layout/manga_controller.xml +++ b/app/src/main/res/layout/manga_controller.xml @@ -48,10 +48,4 @@ android:layout_gravity="bottom" app:layout_dodgeInsetEdges="bottom" /> - - diff --git a/app/src/main/res/layout/manga_info_header.xml b/app/src/main/res/layout/manga_info_header.xml index 92b410e00..78a09cdbc 100644 --- a/app/src/main/res/layout/manga_info_header.xml +++ b/app/src/main/res/layout/manga_info_header.xml @@ -1,161 +1,159 @@ - - + + + android:layout_height="160dp" + android:background="@drawable/manga_info_gradient" + android:backgroundTint="?android:attr/colorBackground" + app:layout_constraintBottom_toBottomOf="@+id/backdrop" /> - + - + + + android:layout_height="60sp" + android:layout_marginBottom="4dp" + android:gravity="bottom" + android:text="@string/manga_info_full_title_label" + android:textIsSelectable="false" + app:autoSizeMaxTextSize="20sp" + app:autoSizeMinTextSize="12sp" + app:autoSizeStepGranularity="2sp" + app:autoSizeTextType="uniform" /> + + + + + android:layout_marginTop="4dp"> - + - + android:layout_marginStart="4dp" + android:layout_marginEnd="4dp" + android:text="•" + android:textIsSelectable="false" + tools:ignore="HardcodedText" /> - - - - - - - - - - - - - - - - - + + + + + + app:icon="@drawable/ic_favorite_border_24dp" /> - - + - + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/manga_info_header_scene.xml b/app/src/main/res/xml/manga_info_header_scene.xml new file mode 100644 index 000000000..8830410d5 --- /dev/null +++ b/app/src/main/res/xml/manga_info_header_scene.xml @@ -0,0 +1,158 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/manga_info_header_scene_sw720dp.xml b/app/src/main/res/xml/manga_info_header_scene_sw720dp.xml new file mode 100644 index 000000000..33ace299a --- /dev/null +++ b/app/src/main/res/xml/manga_info_header_scene_sw720dp.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/manga_summary_section_scene.xml b/app/src/main/res/xml/manga_summary_section_scene.xml new file mode 100644 index 000000000..6dface18b --- /dev/null +++ b/app/src/main/res/xml/manga_summary_section_scene.xml @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +