Details ui updates

master
Koitharu 1 year ago
parent 2c2db1ca96
commit 8c79df3d35
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -18,8 +18,8 @@ android {
applicationId 'org.koitharu.kotatsu' applicationId 'org.koitharu.kotatsu'
minSdk = 21 minSdk = 21
targetSdk = 35 targetSdk = 35
versionCode = 693 versionCode = 700
versionName = '7.7.1' versionName = '8.0-a1'
generatedDensities = [] generatedDensities = []
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner' testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
ksp { ksp {

@ -6,11 +6,17 @@ import android.graphics.Canvas
import android.graphics.drawable.Animatable import android.graphics.drawable.Animatable
import androidx.annotation.StyleRes import androidx.annotation.StyleRes
import androidx.interpolator.view.animation.FastOutSlowInInterpolator import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import coil3.Image
import coil3.asImage
import coil3.getExtra
import coil3.request.ImageRequest
import com.google.android.material.animation.ArgbEvaluatorCompat import com.google.android.material.animation.ArgbEvaluatorCompat
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.util.KotatsuColors import org.koitharu.kotatsu.core.util.KotatsuColors
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
import org.koitharu.kotatsu.core.util.ext.mangaSourceKey
import kotlin.math.abs import kotlin.math.abs
class AnimatedFaviconDrawable( class AnimatedFaviconDrawable(
@ -69,4 +75,16 @@ class AnimatedFaviconDrawable(
colorForeground = ArgbEvaluatorCompat.getInstance() colorForeground = ArgbEvaluatorCompat.getInstance()
.evaluate(interpolator.getInterpolation(fraction), colorLow, colorHigh) .evaluate(interpolator.getInterpolation(fraction), colorLow, colorHigh)
} }
class Factory(
@StyleRes private val styleResId: Int,
) : ((ImageRequest) -> Image?) {
override fun invoke(request: ImageRequest): Image? {
val source = request.getExtra(mangaSourceKey) ?: return null
val context = request.context
val title = source.getTitle(context)
return AnimatedFaviconDrawable(context, styleResId, title).asImage()
}
}
} }

@ -13,9 +13,15 @@ import android.graphics.drawable.Drawable
import androidx.annotation.StyleRes import androidx.annotation.StyleRes
import androidx.core.content.withStyledAttributes import androidx.core.content.withStyledAttributes
import androidx.core.graphics.withClip import androidx.core.graphics.withClip
import coil3.Image
import coil3.asImage
import coil3.getExtra
import coil3.request.ImageRequest
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.util.KotatsuColors import org.koitharu.kotatsu.core.util.KotatsuColors
import org.koitharu.kotatsu.core.util.ext.mangaSourceKey
open class FaviconDrawable( open class FaviconDrawable(
context: Context, context: Context,
@ -29,6 +35,7 @@ open class FaviconDrawable(
private var colorStroke = Color.LTGRAY private var colorStroke = Color.LTGRAY
private val letter = name.take(1).uppercase() private val letter = name.take(1).uppercase()
private var cornerSize = 0f private var cornerSize = 0f
private var intrinsicSize = -1
private val textBounds = Rect() private val textBounds = Rect()
private val tempRect = Rect() private val tempRect = Rect()
private val boundsF = RectF() private val boundsF = RectF()
@ -40,6 +47,7 @@ open class FaviconDrawable(
colorStroke = getColor(R.styleable.FaviconFallbackDrawable_strokeColor, colorStroke) colorStroke = getColor(R.styleable.FaviconFallbackDrawable_strokeColor, colorStroke)
cornerSize = getDimension(R.styleable.FaviconFallbackDrawable_cornerSize, cornerSize) cornerSize = getDimension(R.styleable.FaviconFallbackDrawable_cornerSize, cornerSize)
paint.strokeWidth = getDimension(R.styleable.FaviconFallbackDrawable_strokeWidth, 0f) * 2f paint.strokeWidth = getDimension(R.styleable.FaviconFallbackDrawable_strokeWidth, 0f) * 2f
intrinsicSize = getDimensionPixelSize(R.styleable.FaviconFallbackDrawable_drawableSize, intrinsicSize)
} }
paint.textAlign = Paint.Align.CENTER paint.textAlign = Paint.Align.CENTER
paint.isFakeBoldText = true paint.isFakeBoldText = true
@ -75,6 +83,10 @@ open class FaviconDrawable(
paint.colorFilter = colorFilter paint.colorFilter = colorFilter
} }
override fun getIntrinsicWidth(): Int = intrinsicSize
override fun getIntrinsicHeight(): Int = intrinsicSize
@Suppress("DeprecatedCallableAddReplaceWith") @Suppress("DeprecatedCallableAddReplaceWith")
@Deprecated("Deprecated in Java") @Deprecated("Deprecated in Java")
override fun getOpacity() = PixelFormat.TRANSPARENT override fun getOpacity() = PixelFormat.TRANSPARENT
@ -103,4 +115,16 @@ open class FaviconDrawable(
paint.getTextBounds(text, 0, text.length, tempRect) paint.getTextBounds(text, 0, text.length, tempRect)
return testTextSize * width / tempRect.width() return testTextSize * width / tempRect.width()
} }
class Factory(
@StyleRes private val styleResId: Int,
) : ((ImageRequest) -> Image?) {
override fun invoke(request: ImageRequest): Image? {
val source = request.getExtra(mangaSourceKey) ?: return null
val context = request.context
val title = source.getTitle(context)
return FaviconDrawable(context, styleResId, title).asImage()
}
}
} }

@ -0,0 +1,41 @@
package org.koitharu.kotatsu.core.ui.image
import android.graphics.drawable.Drawable
import android.view.Gravity
import android.widget.TextView
import androidx.annotation.GravityInt
import coil3.target.GenericViewTarget
class TextViewTarget(
override val view: TextView,
@GravityInt compoundDrawable: Int,
) : GenericViewTarget<TextView>() {
private val drawableIndex: Int = when (compoundDrawable) {
Gravity.START -> 0
Gravity.TOP -> 2
Gravity.END -> 3
Gravity.BOTTOM -> 4
else -> -1
}
override var drawable: Drawable?
get() = if (drawableIndex != -1) {
view.compoundDrawablesRelative[drawableIndex]
} else {
null
}
set(value) {
if (drawableIndex == -1) {
return
}
val drawables = view.compoundDrawablesRelative
drawables[drawableIndex] = value
view.setCompoundDrawablesRelativeWithIntrinsicBounds(
drawables[0],
drawables[1],
drawables[2],
drawables[3],
)
}
}

@ -9,6 +9,7 @@ import android.text.style.ForegroundColorSpan
import android.text.style.ImageSpan import android.text.style.ImageSpan
import android.text.style.RelativeSizeSpan import android.text.style.RelativeSizeSpan
import android.transition.TransitionManager import android.transition.TransitionManager
import android.view.Gravity
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
@ -37,6 +38,7 @@ import coil3.request.lifecycle
import coil3.request.placeholder import coil3.request.placeholder
import coil3.request.target import coil3.request.target
import coil3.request.transformations import coil3.request.transformations
import coil3.size.Precision
import coil3.size.Scale import coil3.size.Scale
import coil3.transform.RoundedCornersTransformation import coil3.transform.RoundedCornersTransformation
import coil3.util.CoilUtils import coil3.util.CoilUtils
@ -55,7 +57,6 @@ import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.LocalMangaSource import org.koitharu.kotatsu.core.model.LocalMangaSource
import org.koitharu.kotatsu.core.model.UnknownMangaSource import org.koitharu.kotatsu.core.model.UnknownMangaSource
import org.koitharu.kotatsu.core.model.getTitle import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.model.iconResId
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.model.titleResId import org.koitharu.kotatsu.core.model.titleResId
import org.koitharu.kotatsu.core.os.AppShortcutManager import org.koitharu.kotatsu.core.os.AppShortcutManager
@ -64,8 +65,9 @@ import org.koitharu.kotatsu.core.parser.favicon.faviconUri
import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.BaseListAdapter import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.OnContextClickListenerCompat import org.koitharu.kotatsu.core.ui.OnContextClickListenerCompat
import org.koitharu.kotatsu.core.ui.image.ChipIconTarget
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
import org.koitharu.kotatsu.core.ui.image.TextViewTarget
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.sheet.BottomSheetCollapseCallback import org.koitharu.kotatsu.core.ui.sheet.BottomSheetCollapseCallback
import org.koitharu.kotatsu.core.ui.util.MenuInvalidator import org.koitharu.kotatsu.core.ui.util.MenuInvalidator
@ -86,9 +88,9 @@ import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.parentView import org.koitharu.kotatsu.core.util.ext.parentView
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
import org.koitharu.kotatsu.core.util.ext.setNavigationBarTransparentCompat import org.koitharu.kotatsu.core.util.ext.setNavigationBarTransparentCompat
import org.koitharu.kotatsu.core.util.ext.setOnContextClickListenerCompat
import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ActivityDetailsBinding import org.koitharu.kotatsu.databinding.ActivityDetailsBinding
import org.koitharu.kotatsu.databinding.LayoutDetailsTableBinding
import org.koitharu.kotatsu.details.data.MangaDetails import org.koitharu.kotatsu.details.data.MangaDetails
import org.koitharu.kotatsu.details.data.ReadingTime import org.koitharu.kotatsu.details.data.ReadingTime
import org.koitharu.kotatsu.details.service.MangaPrefetchService import org.koitharu.kotatsu.details.service.MangaPrefetchService
@ -112,13 +114,12 @@ import org.koitharu.kotatsu.local.ui.info.LocalInfoDialog
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.util.ellipsize
import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet
import org.koitharu.kotatsu.search.ui.MangaListActivity import org.koitharu.kotatsu.search.ui.MangaListActivity
import org.koitharu.kotatsu.stats.ui.sheet.MangaStatsSheet
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.roundToInt
import com.google.android.material.R as materialR import com.google.android.material.R as materialR
@AndroidEntryPoint @AndroidEntryPoint
@ -140,30 +141,24 @@ class DetailsActivity :
private val viewModel: DetailsViewModel by viewModels() private val viewModel: DetailsViewModel by viewModels()
private lateinit var menuProvider: DetailsMenuProvider private lateinit var menuProvider: DetailsMenuProvider
private lateinit var infoBinding: LayoutDetailsTableBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(ActivityDetailsBinding.inflate(layoutInflater)) setContentView(ActivityDetailsBinding.inflate(layoutInflater))
infoBinding = LayoutDetailsTableBinding.bind(viewBinding.root)
supportActionBar?.run { supportActionBar?.run {
setDisplayHomeAsUpEnabled(true) setDisplayHomeAsUpEnabled(true)
setDisplayShowTitleEnabled(false) setDisplayShowTitleEnabled(false)
} }
viewBinding.buttonRead.setOnClickListener(this) viewBinding.chipFavorite.setOnClickListener(this)
viewBinding.buttonRead.setOnLongClickListener(this) infoBinding.textViewLocal.setOnClickListener(this)
viewBinding.buttonRead.setOnContextClickListenerCompat(this) infoBinding.textViewAuthor.setOnClickListener(this)
viewBinding.buttonDownload?.setOnClickListener(this) infoBinding.textViewSource.setOnClickListener(this)
viewBinding.infoLayout.chipBranch.setOnClickListener(this)
viewBinding.infoLayout.chipSize.setOnClickListener(this)
viewBinding.infoLayout.chipSource.setOnClickListener(this)
viewBinding.infoLayout.chipFavorite.setOnClickListener(this)
viewBinding.infoLayout.chipAuthor.setOnClickListener(this)
viewBinding.infoLayout.chipTime.setOnClickListener(this)
viewBinding.imageViewCover.setOnClickListener(this) viewBinding.imageViewCover.setOnClickListener(this)
viewBinding.buttonDescriptionMore.setOnClickListener(this) viewBinding.buttonDescriptionMore.setOnClickListener(this)
viewBinding.buttonScrobblingMore.setOnClickListener(this) viewBinding.buttonScrobblingMore.setOnClickListener(this)
viewBinding.buttonRelatedMore.setOnClickListener(this) viewBinding.buttonRelatedMore.setOnClickListener(this)
viewBinding.infoLayout.chipSource.setOnClickListener(this)
viewBinding.infoLayout.chipSize.setOnClickListener(this)
viewBinding.textViewDescription.addOnLayoutChangeListener(this) viewBinding.textViewDescription.addOnLayoutChangeListener(this)
viewBinding.swipeRefreshLayout.setOnRefreshListener(this) viewBinding.swipeRefreshLayout.setOnRefreshListener(this)
viewBinding.textViewDescription.viewTreeObserver.addOnDrawListener(this) viewBinding.textViewDescription.viewTreeObserver.addOnDrawListener(this)
@ -191,17 +186,17 @@ class DetailsActivity :
viewModel.localSize.observe(this, ::onLocalSizeChanged) viewModel.localSize.observe(this, ::onLocalSizeChanged)
viewModel.relatedManga.observe(this, ::onRelatedMangaChanged) viewModel.relatedManga.observe(this, ::onRelatedMangaChanged)
viewModel.readingTime.observe(this, ::onReadingTimeChanged) viewModel.readingTime.observe(this, ::onReadingTimeChanged)
viewModel.selectedBranch.observe(this) { // viewModel.selectedBranch.observe(this) {
viewBinding.infoLayout.chipBranch.text = it.ifNullOrEmpty { getString(R.string.system_default) } // viewBinding.infoLayout?.chipBranch?.text = it.ifNullOrEmpty { getString(R.string.system_default) }
} // }
viewModel.favouriteCategories.observe(this, ::onFavoritesChanged) viewModel.favouriteCategories.observe(this, ::onFavoritesChanged)
val menuInvalidator = MenuInvalidator(this) val menuInvalidator = MenuInvalidator(this)
viewModel.isStatsAvailable.observe(this, menuInvalidator) viewModel.isStatsAvailable.observe(this, menuInvalidator)
viewModel.remoteManga.observe(this, menuInvalidator) viewModel.remoteManga.observe(this, menuInvalidator)
viewModel.branches.observe(this) { // viewModel.branches.observe(this) {
viewBinding.infoLayout.chipBranch.isVisible = it.size > 1 || !it.firstOrNull()?.name.isNullOrEmpty() // viewBinding.infoLayout?.chipBranch?.isVisible = it.size > 1 || !it.firstOrNull()?.name.isNullOrEmpty()
viewBinding.infoLayout.chipBranch.isCloseIconVisible = it.size > 1 // viewBinding.infoLayout?.chipBranch?.isCloseIconVisible = it.size > 1
} // }
viewModel.chapters.observe(this, PrefetchObserver(this)) viewModel.chapters.observe(this, PrefetchObserver(this))
viewModel.onDownloadStarted viewModel.onDownloadStarted
.filterNot { ChaptersPagesSheet.isShown(supportFragmentManager) } .filterNot { ChaptersPagesSheet.isShown(supportFragmentManager) }
@ -221,14 +216,9 @@ class DetailsActivity :
override fun onClick(v: View) { override fun onClick(v: View) {
when (v.id) { when (v.id) {
R.id.button_read -> openReader(isIncognitoMode = false) // R.id.chip_branch -> showBranchPopupMenu(v)
R.id.chip_branch -> showBranchPopupMenu(v)
R.id.button_download -> {
val manga = viewModel.manga.value ?: return
DownloadDialogFragment.show(supportFragmentManager, listOf(manga))
}
R.id.chip_author -> { R.id.textView_author -> {
val manga = viewModel.manga.value ?: return val manga = viewModel.manga.value ?: return
startActivity( startActivity(
MangaListActivity.newIntent( MangaListActivity.newIntent(
@ -239,7 +229,7 @@ class DetailsActivity :
) )
} }
R.id.chip_source -> { R.id.textView_source -> {
val manga = viewModel.manga.value ?: return val manga = viewModel.manga.value ?: return
startActivity( startActivity(
MangaListActivity.newIntent( MangaListActivity.newIntent(
@ -250,7 +240,7 @@ class DetailsActivity :
) )
} }
R.id.chip_size -> { R.id.textView_local -> {
val manga = viewModel.manga.value ?: return val manga = viewModel.manga.value ?: return
LocalInfoDialog.show(supportFragmentManager, manga) LocalInfoDialog.show(supportFragmentManager, manga)
} }
@ -260,14 +250,14 @@ class DetailsActivity :
FavoriteSheet.show(supportFragmentManager, manga) FavoriteSheet.show(supportFragmentManager, manga)
} }
R.id.chip_time -> { // R.id.chip_time -> {
if (viewModel.isStatsAvailable.value) { // if (viewModel.isStatsAvailable.value) {
val manga = viewModel.manga.value ?: return // val manga = viewModel.manga.value ?: return
MangaStatsSheet.show(supportFragmentManager, manga) // MangaStatsSheet.show(supportFragmentManager, manga)
} else { // } else {
// TODO // // TODO
} // }
} // }
R.id.imageView_cover -> { R.id.imageView_cover -> {
val manga = viewModel.manga.value ?: return val manga = viewModel.manga.value ?: return
@ -378,7 +368,7 @@ class DetailsActivity :
} }
private fun onFavoritesChanged(categories: Set<FavouriteCategory>) { private fun onFavoritesChanged(categories: Set<FavouriteCategory>) {
val chip = viewBinding.infoLayout.chipFavorite val chip = viewBinding.chipFavorite
chip.setChipIconResource(if (categories.isEmpty()) R.drawable.ic_heart_outline else R.drawable.ic_heart) chip.setChipIconResource(if (categories.isEmpty()) R.drawable.ic_heart_outline else R.drawable.ic_heart)
chip.text = if (categories.isEmpty()) { chip.text = if (categories.isEmpty()) {
getString(R.string.add_to_favourites) getString(R.string.add_to_favourites)
@ -388,17 +378,18 @@ class DetailsActivity :
} }
private fun onReadingTimeChanged(time: ReadingTime?) { private fun onReadingTimeChanged(time: ReadingTime?) {
val chip = viewBinding.infoLayout.chipTime // TODO
chip.textAndVisible = time?.formatShort(chip.resources) // chip.textAndVisible = time?.formatShort(chip.resources)
} }
private fun onLocalSizeChanged(size: Long) { private fun onLocalSizeChanged(size: Long) {
val chip = viewBinding.infoLayout.chipSize
if (size == 0L) { if (size == 0L) {
chip.isVisible = false infoBinding.textViewLocal.isVisible = false
infoBinding.textViewLocalLabel.isVisible = false
} else { } else {
chip.text = FileSize.BYTES.format(chip.context, size) infoBinding.textViewLocal.text = FileSize.BYTES.format(this, size)
chip.isVisible = true infoBinding.textViewLocal.isVisible = true
infoBinding.textViewLocalLabel.isVisible = true
} }
} }
@ -442,63 +433,60 @@ class DetailsActivity :
} }
private fun onMangaUpdated(details: MangaDetails) { private fun onMangaUpdated(details: MangaDetails) {
with(viewBinding) {
val manga = details.toManga() val manga = details.toManga()
// Main
loadCover(manga) loadCover(manga)
with(viewBinding) {
textViewTitle.text = manga.title textViewTitle.text = manga.title
textViewSubtitle.textAndVisible = manga.altTitle textViewSubtitle.textAndVisible = manga.altTitle
infoLayout.chipAuthor.textAndVisible = manga.author?.ellipsize(AUTHOR_LABEL_LIMIT) textViewNsfw.isVisible = manga.isNsfw
textViewDescription.text = details.description.ifNullOrEmpty { getString(R.string.no_description) }
}
with(infoBinding) {
textViewAuthor.textAndVisible = manga.author
textViewAuthorLabel.isVisible = textViewAuthor.isVisible
if (manga.hasRating) { if (manga.hasRating) {
ratingBar.rating = manga.rating * ratingBar.numStars ratingBarRating.rating = manga.rating * ratingBarRating.numStars
ratingBar.isVisible = true ratingBarRating.isVisible = true
textViewRatingLabel.isVisible = true
} else { } else {
ratingBar.isVisible = false ratingBarRating.isVisible = false
textViewRatingLabel.isVisible = false
} }
manga.state?.let { state -> manga.state?.let { state ->
textViewState.textAndVisible = resources.getString(state.titleResId) textViewState.textAndVisible = resources.getString(state.titleResId)
imageViewState.setImageResource(state.iconResId) textViewStateLabel.isVisible = textViewState.isVisible
imageViewState.isVisible = true
} ?: run { } ?: run {
textViewState.isVisible = false textViewState.isVisible = false
imageViewState.isVisible = false textViewStateLabel.isVisible = false
} }
if (manga.source == LocalMangaSource || manga.source == UnknownMangaSource) { if (manga.source == LocalMangaSource || manga.source == UnknownMangaSource) {
infoLayout.chipSource.isVisible = false textViewSource.isVisible = false
textViewSourceLabel.isVisible = false
} else { } else {
infoLayout.chipSource.text = manga.source.getTitle(this@DetailsActivity) textViewSource.textAndVisible = manga.source.getTitle(this@DetailsActivity)
infoLayout.chipSource.isVisible = true textViewSourceLabel.isVisible = textViewSource.isVisible == true
} }
val faviconPlaceholderFactory = FaviconDrawable.Factory(R.style.FaviconDrawable_Chip)
textViewNsfw.isVisible = manga.isNsfw
// Chips
bindTags(manga)
textViewDescription.text = details.description.ifNullOrEmpty { getString(R.string.no_description) }
viewBinding.infoLayout.chipSource.also { chip ->
ImageRequest.Builder(this@DetailsActivity) ImageRequest.Builder(this@DetailsActivity)
.data(manga.source.faviconUri()) .data(manga.source.faviconUri())
.lifecycle(this@DetailsActivity) .lifecycle(this@DetailsActivity)
.crossfade(false) .crossfade(false)
.precision(Precision.EXACT)
.size(resources.getDimensionPixelSize(materialR.dimen.m3_chip_icon_size)) .size(resources.getDimensionPixelSize(materialR.dimen.m3_chip_icon_size))
.target(ChipIconTarget(chip)) .target(TextViewTarget(textViewSource, Gravity.START))
.placeholder(R.drawable.ic_web) .placeholder(faviconPlaceholderFactory)
.fallback(R.drawable.ic_web) .error(faviconPlaceholderFactory)
.error(R.drawable.ic_web) .fallback(faviconPlaceholderFactory)
.mangaSourceExtra(manga.source) .mangaSourceExtra(manga.source)
.transformations(RoundedCornersTransformation(resources.getDimension(R.dimen.chip_icon_corner))) .transformations(RoundedCornersTransformation(resources.getDimension(R.dimen.chip_icon_corner)))
.allowRgb565(true) .allowRgb565(true)
.enqueueWith(coil) .enqueueWith(coil)
} }
bindTags(manga)
title = manga.title title = manga.title
invalidateOptionsMenu() invalidateOptionsMenu()
} }
}
private fun onMangaRemoved(manga: Manga) { private fun onMangaRemoved(manga: Manga) {
Toast.makeText( Toast.makeText(
@ -527,22 +515,28 @@ class DetailsActivity :
} }
} }
private fun onHistoryChanged(info: HistoryInfo, isLoading: Boolean) = with(viewBinding) { private fun onHistoryChanged(info: HistoryInfo, isLoading: Boolean) = with(infoBinding) {
buttonRead.setTitle(if (info.canContinue) R.string._continue else R.string.read) textViewChapters.textAndVisible = when {
buttonRead.subtitle = when { isLoading -> null
isLoading -> getString(R.string.loading_)
info.isIncognitoMode -> getString(R.string.incognito_mode)
info.isChapterMissing -> getString(R.string.chapter_is_missing)
info.currentChapter >= 0 -> getString(R.string.chapter_d_of_d, info.currentChapter + 1, info.totalChapters) info.currentChapter >= 0 -> getString(R.string.chapter_d_of_d, info.currentChapter + 1, info.totalChapters)
info.totalChapters == 0 -> getString(R.string.no_chapters) info.totalChapters == 0 -> getString(R.string.no_chapters)
info.totalChapters == -1 -> getString(R.string.error_occurred) info.totalChapters == -1 -> getString(R.string.error_occurred)
else -> resources.getQuantityString(R.plurals.chapters, info.totalChapters, info.totalChapters) else -> resources.getQuantityString(R.plurals.chapters, info.totalChapters, info.totalChapters)
} }
val isFirstCall = buttonRead.tag == null textViewProgress.textAndVisible = if (info.percent <= 0f) {
buttonRead.tag = Unit null
buttonRead.setProgress(info.percent.coerceIn(0f, 1f), !isFirstCall) } else {
buttonDownload?.isEnabled = info.isValid && info.canDownload getString(R.string.percent_string_pattern, (info.percent * 100f).toInt().toString())
buttonRead.isEnabled = info.isValid }
progress.setProgressCompat(
(progress.max * info.percent.coerceIn(0f, 1f)).roundToInt(),
true,
)
textViewProgressLabel.isVisible = info.history != null
textViewProgress.isVisible = info.history != null
progress.isVisible = info.history != null
// buttonRead.setProgress(info.percent.coerceIn(0f, 1f), !isFirstCall)
// buttonDownload?.isEnabled = info.isValid && info.canDownload
} }
private fun showBranchPopupMenu(v: View) { private fun showBranchPopupMenu(v: View) {
@ -663,7 +657,6 @@ class DetailsActivity :
companion object { companion object {
private const val FAV_LABEL_LIMIT = 16 private const val FAV_LABEL_LIMIT = 16
private const val AUTHOR_LABEL_LIMIT = 16
fun newIntent(context: Context, manga: Manga): Intent { fun newIntent(context: Context, manga: Manga): Intent {
return Intent(context, DetailsActivity::class.java) return Intent(context, DetailsActivity::class.java)

@ -0,0 +1,147 @@
package org.koitharu.kotatsu.details.ui
import android.content.Context
import android.graphics.Color
import android.text.style.DynamicDrawableSpan
import android.text.style.ForegroundColorSpan
import android.text.style.ImageSpan
import android.text.style.RelativeSizeSpan
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.Toast
import androidx.appcompat.widget.PopupMenu
import androidx.core.text.buildSpannedString
import androidx.core.text.inSpans
import androidx.core.view.get
import androidx.lifecycle.LifecycleOwner
import com.google.android.material.button.MaterialButton
import com.google.android.material.button.MaterialSplitButton
import com.google.android.material.snackbar.Snackbar
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.details.ui.model.HistoryInfo
import org.koitharu.kotatsu.reader.ui.ReaderActivity
class ReadButtonDelegate(
splitButton: MaterialSplitButton,
private val viewModel: DetailsViewModel,
) : View.OnClickListener, PopupMenu.OnMenuItemClickListener, PopupMenu.OnDismissListener {
private val buttonRead = splitButton[0] as MaterialButton
private val buttonMenu = splitButton[1] as MaterialButton
private val context: Context
get() = buttonRead.context
override fun onClick(v: View) {
when (v.id) {
R.id.button_read -> openReader(isIncognitoMode = false)
R.id.button_read_menu -> showMenu()
}
}
override fun onMenuItemClick(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_incognito -> openReader(isIncognitoMode = true)
R.id.action_forget -> viewModel.removeFromHistory()
else -> {
val branch = viewModel.branches.value.getOrNull(item.order) ?: return false
viewModel.setSelectedBranch(branch.name)
}
}
return true
}
override fun onDismiss(menu: PopupMenu?) {
buttonMenu.isChecked = false
}
fun attach(lifecycleOwner: LifecycleOwner) {
buttonRead.setOnClickListener(this)
buttonMenu.setOnClickListener(this)
viewModel.historyInfo.observe(lifecycleOwner, this::onHistoryChanged)
}
private fun showMenu() {
val menu = PopupMenu(context, buttonMenu)
menu.inflate(R.menu.popup_read)
menu.menu.setGroupDividerEnabled(true)
menu.menu.populateBranchList()
menu.menu.findItem(R.id.action_forget)?.isVisible = viewModel.historyInfo.value.run {
!isIncognitoMode && history != null
}
menu.setOnMenuItemClickListener(this)
menu.setForceShowIcon(true)
menu.setOnDismissListener(this)
buttonMenu.isChecked = true
menu.show()
}
private fun openReader(isIncognitoMode: Boolean) {
val detailsViewModel = viewModel as? DetailsViewModel ?: return
val manga = viewModel.manga.value ?: return
if (detailsViewModel.historyInfo.value.isChapterMissing) {
Snackbar.make(buttonRead, R.string.chapter_is_missing, Snackbar.LENGTH_SHORT)
.show() // TODO
} else {
context.startActivity(
ReaderActivity.IntentBuilder(context)
.manga(manga)
.branch(detailsViewModel.selectedBranchValue)
.incognito(isIncognitoMode)
.build(),
)
if (isIncognitoMode) {
Toast.makeText(context, R.string.incognito_mode, Toast.LENGTH_SHORT).show()
}
}
}
private fun onHistoryChanged(info: HistoryInfo) {
buttonRead.setText(if (info.canContinue) R.string._continue else R.string.read)
buttonRead.isEnabled = info.isValid
}
private fun Menu.populateBranchList() {
val branches = viewModel.branches.value
if (branches.size <= 1) {
return
}
for ((i, branch) in branches.withIndex()) {
val title = buildSpannedString {
if (branch.isCurrent) {
inSpans(
ImageSpan(
context,
R.drawable.ic_current_chapter,
DynamicDrawableSpan.ALIGN_BASELINE,
),
) {
append(' ')
}
append(' ')
}
append(branch.name ?: context.getString(R.string.system_default))
append(' ')
append(' ')
inSpans(
ForegroundColorSpan(
context.getThemeColor(
android.R.attr.textColorSecondary,
Color.LTGRAY,
),
),
RelativeSizeSpan(0.74f),
) {
append(branch.count.toString())
}
}
val item = add(R.id.group_branches, Menu.NONE, i, title)
item.isCheckable = true
item.isChecked = branch.isSelected
}
setGroupCheckable(R.id.group_branches, true, true)
}
}

@ -25,13 +25,21 @@ class ChaptersPagesAdapter(
} }
override fun onConfigureTab(tab: TabLayout.Tab, position: Int) { override fun onConfigureTab(tab: TabLayout.Tab, position: Int) {
tab.setText( tab.setIcon(
when (position) { when (position) {
0 -> R.string.chapters 0 -> R.drawable.ic_list
1 -> if (isPagesTabEnabled) R.string.pages else R.string.bookmarks 1 -> if (isPagesTabEnabled) R.drawable.ic_grid else R.drawable.ic_bookmark
2 -> R.string.bookmarks 2 -> R.drawable.ic_bookmark
else -> 0 else -> 0
}, },
) )
// tab.setText(
// when (position) {
// 0 -> R.string.chapters
// 1 -> if (isPagesTabEnabled) R.string.pages else R.string.bookmarks
// 2 -> R.string.bookmarks
// else -> 0
// },
// )
} }
} }

@ -29,6 +29,8 @@ import org.koitharu.kotatsu.core.util.ext.setTabsEnabled
import org.koitharu.kotatsu.core.util.ext.showDistinct import org.koitharu.kotatsu.core.util.ext.showDistinct
import org.koitharu.kotatsu.core.util.ext.withArgs import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.SheetChaptersPagesBinding import org.koitharu.kotatsu.databinding.SheetChaptersPagesBinding
import org.koitharu.kotatsu.details.ui.DetailsViewModel
import org.koitharu.kotatsu.details.ui.ReadButtonDelegate
import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver
import javax.inject.Inject import javax.inject.Inject
@ -54,6 +56,9 @@ class ChaptersPagesSheet : BaseAdaptiveSheet<SheetChaptersPagesBinding>(), Actio
if (!adapter.isPagesTabEnabled) { if (!adapter.isPagesTabEnabled) {
defaultTab = (defaultTab - 1).coerceAtLeast(TAB_CHAPTERS) defaultTab = (defaultTab - 1).coerceAtLeast(TAB_CHAPTERS)
} }
(viewModel as? DetailsViewModel)?.let { dvm ->
ReadButtonDelegate(binding.splitButtonRead, dvm).attach(viewLifecycleOwner)
}
binding.pager.offscreenPageLimit = adapter.itemCount binding.pager.offscreenPageLimit = adapter.itemCount
binding.pager.recyclerView?.isNestedScrollingEnabled = false binding.pager.recyclerView?.isNestedScrollingEnabled = false
binding.pager.adapter = adapter binding.pager.adapter = adapter
@ -88,6 +93,8 @@ class ChaptersPagesSheet : BaseAdaptiveSheet<SheetChaptersPagesBinding>(), Actio
val binding = viewBinding ?: return val binding = viewBinding ?: return
val isActionModeStarted = actionModeDelegate?.isActionModeStarted == true val isActionModeStarted = actionModeDelegate?.isActionModeStarted == true
binding.toolbar.menuView?.isVisible = newState != STATE_COLLAPSED && !isActionModeStarted binding.toolbar.menuView?.isVisible = newState != STATE_COLLAPSED && !isActionModeStarted
binding.splitButtonRead.isVisible = newState == STATE_COLLAPSED && !isActionModeStarted
&& viewModel is DetailsViewModel
} }
override fun onActionModeStarted(mode: ActionMode) { override fun onActionModeStarted(mode: ActionMode) {

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:bottom="5dp"
android:left="5dp"
android:right="5dp"
android:top="5dp">
<shape android:shape="rectangle">
<solid android:color="@android:color/white" />
<corners
android:bottomLeftRadius="@dimen/list_selector_corner"
android:bottomRightRadius="@dimen/list_selector_corner"
android:topLeftRadius="@dimen/list_selector_corner"
android:topRightRadius="@dimen/list_selector_corner" />
</shape>
</item>
</layer-list>

@ -120,56 +120,17 @@
app:layout_constraintTop_toBottomOf="@id/textView_title" app:layout_constraintTop_toBottomOf="@id/textView_title"
tools:text="@tools:sample/lorem[12]" /> tools:text="@tools:sample/lorem[12]" />
<ImageView <com.google.android.material.chip.Chip
android:id="@+id/imageView_state" android:id="@+id/chip_favorite"
android:layout_width="0dp" style="@style/Widget.Kotatsu.Chip.Dropdown"
android:layout_height="0dp"
android:layout_marginVertical="0.5dp"
android:layout_marginStart="16dp"
app:layout_constraintBottom_toBottomOf="@id/textView_state"
app:layout_constraintDimensionRatio="1"
app:layout_constraintStart_toEndOf="@id/imageView_cover"
app:layout_constraintTop_toTopOf="@id/textView_state"
tools:src="@drawable/ic_state_ongoing" />
<TextView
android:id="@+id/textView_state"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="16dp"
android:drawablePadding="4dp"
android:gravity="center_vertical"
android:labelFor="@id/imageView_state"
android:singleLine="true"
android:textColor="?colorTertiary"
android:textStyle="bold"
app:drawableTint="?colorTertiary"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@id/imageView_state"
app:layout_constraintTop_toBottomOf="@id/textView_subtitle"
tools:text="@string/state_ongoing" />
<RatingBar
android:id="@+id/rating_bar"
style="?ratingBarStyleSmall"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginTop="4dp" android:layout_marginTop="8dp"
android:layout_marginEnd="16dp" app:chipIcon="@drawable/ic_heart_outline"
android:isIndicator="true"
android:max="1"
android:numStars="5"
android:stepSize="0.5"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@id/imageView_cover" app:layout_constraintStart_toEndOf="@id/imageView_cover"
app:layout_constraintTop_toBottomOf="@id/textView_state" app:layout_constraintTop_toBottomOf="@id/textView_subtitle"
tools:rating="4" /> tools:text="@string/add_to_favourites" />
<androidx.constraintlayout.widget.Barrier <androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier_header" android:id="@+id/barrier_header"
@ -177,41 +138,9 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:barrierDirection="bottom" app:barrierDirection="bottom"
app:barrierMargin="@dimen/margin_normal" app:barrierMargin="@dimen/margin_normal"
app:constraint_referenced_ids="imageView_cover,rating_bar" /> app:constraint_referenced_ids="imageView_cover,chip_favorite" />
<include
android:id="@+id/info_layout"
layout="@layout/layout_details_chips"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/screen_padding"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/barrier_header" />
<org.koitharu.kotatsu.core.ui.widgets.ProgressButton <include layout="@layout/layout_details_table" />
android:id="@+id/button_read"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="end|center_vertical"
android:layout_marginHorizontal="@dimen/screen_padding"
android:layout_marginTop="@dimen/margin_normal"
android:foreground="?selectableItemBackground"
android:gravity="center"
android:paddingHorizontal="6dp"
android:paddingVertical="8dp"
android:textColor="?colorOnPrimaryContainer"
app:baseColor="?colorSecondaryContainer"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/info_layout"
app:progressColor="?colorPrimary"
app:subtitleTextAppearance="?textAppearanceBodySmall"
app:titleTextAppearance="?textAppearanceButton"
tools:max="100"
tools:progress="40"
tools:subtitle="12 chapters"
tools:title="@string/read" />
<TextView <TextView
android:id="@+id/textView_description_title" android:id="@+id/textView_description_title"
@ -226,7 +155,7 @@
android:textAppearance="?textAppearanceTitleSmall" android:textAppearance="?textAppearanceTitleSmall"
app:layout_constraintEnd_toStartOf="@id/button_description_more" app:layout_constraintEnd_toStartOf="@id/button_description_more"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/button_read" /> app:layout_constraintTop_toBottomOf="@id/textView_progress_label" />
<Button <Button
android:id="@+id/button_description_more" android:id="@+id/button_description_more"

@ -113,57 +113,17 @@
app:layout_constraintTop_toBottomOf="@id/textView_title" app:layout_constraintTop_toBottomOf="@id/textView_title"
tools:text="@tools:sample/lorem[12]" /> tools:text="@tools:sample/lorem[12]" />
<ImageView <com.google.android.material.chip.Chip
android:id="@+id/imageView_state" android:id="@+id/chip_favorite"
android:layout_width="0dp" style="@style/Widget.Kotatsu.Chip.Dropdown"
android:layout_height="0dp"
android:layout_marginVertical="0.5dp"
android:layout_marginStart="16dp"
app:layout_constraintBottom_toBottomOf="@id/textView_state"
app:layout_constraintDimensionRatio="1"
app:layout_constraintStart_toEndOf="@id/imageView_cover"
app:layout_constraintTop_toTopOf="@id/textView_state"
app:tint="?colorTertiary"
tools:src="@drawable/ic_state_ongoing" />
<TextView
android:id="@+id/textView_state"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="16dp"
android:drawablePadding="4dp"
android:gravity="center_vertical"
android:labelFor="@id/imageView_state"
android:singleLine="true"
android:textColor="?colorTertiary"
android:textStyle="bold"
app:drawableTint="?colorTertiary"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@id/imageView_state"
app:layout_constraintTop_toBottomOf="@id/textView_subtitle"
tools:text="@string/state_ongoing" />
<RatingBar
android:id="@+id/rating_bar"
style="?ratingBarStyleSmall"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginTop="4dp" android:layout_marginTop="8dp"
android:layout_marginEnd="16dp" app:chipIcon="@drawable/ic_heart_outline"
android:isIndicator="true"
android:max="1"
android:numStars="5"
android:stepSize="0.5"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@id/imageView_cover" app:layout_constraintStart_toEndOf="@id/imageView_cover"
app:layout_constraintTop_toBottomOf="@id/textView_state" app:layout_constraintTop_toBottomOf="@id/textView_subtitle"
tools:rating="4" /> tools:text="@string/add_to_favourites" />
<androidx.constraintlayout.widget.Barrier <androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier_header" android:id="@+id/barrier_header"
@ -171,55 +131,9 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:barrierDirection="bottom" app:barrierDirection="bottom"
app:barrierMargin="@dimen/margin_normal" app:barrierMargin="@dimen/margin_normal"
app:constraint_referenced_ids="imageView_cover,rating_bar" /> app:constraint_referenced_ids="imageView_cover,chip_favorite" />
<include <include layout="@layout/layout_details_table" />
android:id="@+id/info_layout"
layout="@layout/layout_details_chips"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/screen_padding"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/barrier_header" />
<org.koitharu.kotatsu.core.ui.widgets.ProgressButton
android:id="@+id/button_read"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="@dimen/margin_normal"
android:layout_marginEnd="12dp"
android:foreground="?selectableItemBackground"
android:gravity="center"
android:paddingHorizontal="6dp"
android:paddingVertical="8dp"
app:baseColor="?colorSecondaryContainer"
app:layout_constraintEnd_toStartOf="@id/button_download"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/info_layout"
app:progressColor="?colorPrimary"
app:subtitleTextAppearance="?textAppearanceBodySmall"
app:titleTextAppearance="?textAppearanceButton"
tools:max="100"
tools:progress="40"
tools:subtitle="12 chapters"
tools:title="@string/read" />
<ImageView
android:id="@+id/button_download"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginEnd="16dp"
android:background="@drawable/bg_circle_button"
android:backgroundTint="?colorSecondaryContainer"
android:contentDescription="@string/download"
android:scaleType="centerInside"
app:layout_constraintBottom_toBottomOf="@id/button_read"
app:layout_constraintDimensionRatio="1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/button_read"
app:srcCompat="@drawable/ic_download" />
<TextView <TextView
android:id="@+id/textView_description_title" android:id="@+id/textView_description_title"
@ -235,7 +149,7 @@
android:textAppearance="?textAppearanceTitleSmall" android:textAppearance="?textAppearanceTitleSmall"
app:layout_constraintEnd_toStartOf="@id/button_description_more" app:layout_constraintEnd_toStartOf="@id/button_description_more"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/button_read" /> app:layout_constraintTop_toBottomOf="@id/textView_progress_label" />
<Button <Button
android:id="@+id/button_description_more" android:id="@+id/button_description_more"
@ -323,21 +237,6 @@
tools:listitem="@layout/item_scrobbling_info" tools:listitem="@layout/item_scrobbling_info"
tools:visibility="visible" /> tools:visibility="visible" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progressBar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="gone"
app:hideAnimationBehavior="outward"
app:layout_constraintBottom_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:showAnimationBehavior="inward"
app:trackCornerRadius="0dp"
tools:visibility="visible" />
<androidx.constraintlayout.widget.Group <androidx.constraintlayout.widget.Group
android:id="@+id/group_scrobbling" android:id="@+id/group_scrobbling"
android:layout_width="wrap_content" android:layout_width="wrap_content"

@ -1,76 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.chip.ChipGroup
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/info_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:chipSpacingHorizontal="6dp"
app:chipSpacingVertical="6dp">
<com.google.android.material.chip.Chip
android:id="@+id/chip_favorite"
style="@style/Widget.Kotatsu.Chip.Dropdown"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:chipIcon="@drawable/ic_heart_outline"
app:ensureMinTouchTargetSize="false"
tools:text="@string/add_to_favourites" />
<com.google.android.material.chip.Chip
android:id="@+id/chip_source"
style="@style/Widget.Kotatsu.Chip.Assist"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:chipIcon="@drawable/ic_web"
app:ensureMinTouchTargetSize="false"
tools:text="Source"
tools:visibility="visible" />
<com.google.android.material.chip.Chip
android:id="@+id/chip_author"
style="@style/Widget.Kotatsu.Chip.Assist"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:chipIcon="@drawable/ic_user"
app:ensureMinTouchTargetSize="false"
tools:text="Author"
tools:visibility="visible" />
<com.google.android.material.chip.Chip
android:id="@+id/chip_size"
style="@style/Widget.Kotatsu.Chip.Assist"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:chipIcon="@drawable/ic_storage"
app:ensureMinTouchTargetSize="false"
tools:text="1.8 GiB"
tools:visibility="visible" />
<com.google.android.material.chip.Chip
android:id="@+id/chip_branch"
style="@style/Widget.Kotatsu.Chip.Dropdown"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:chipIcon="@drawable/ic_language"
app:ensureMinTouchTargetSize="false"
tools:text="English"
tools:visibility="visible" />
<com.google.android.material.chip.Chip
android:id="@+id/chip_time"
style="@style/Widget.Kotatsu.Chip.Assist"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:chipIcon="@drawable/ic_timelapse"
app:ensureMinTouchTargetSize="false"
tools:text="2 h 40 m"
tools:visibility="visible" />
</com.google.android.material.chip.ChipGroup>

@ -0,0 +1,235 @@
<?xml version="1.0" encoding="utf-8"?>
<merge
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"
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_details"
style="?materialCardViewFilledStyle"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginHorizontal="@dimen/screen_padding"
android:layout_marginBottom="-12dp"
app:cardBackgroundColor="?colorBackgroundFloating"
app:layout_constraintBottom_toBottomOf="@id/textView_progress_label"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/barrier_header" />
<TextView
android:id="@+id/textView_source_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="12dp"
android:singleLine="true"
android:text="@string/source"
android:textAppearance="?textAppearanceTitleSmall"
app:layout_constraintStart_toStartOf="@id/card_details"
app:layout_constraintTop_toBottomOf="@id/barrier_header" />
<TextView
android:id="@+id/textView_source"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:background="@drawable/custom_selectable_item_background"
android:drawablePadding="4dp"
android:padding="4dp"
android:singleLine="true"
android:textAppearance="?textAppearanceBodyMedium"
app:layout_constraintBaseline_toBaselineOf="@id/textView_source_label"
app:layout_constraintEnd_toEndOf="@id/card_details"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@id/barrier_table"
tools:text="MangaSource" />
<TextView
android:id="@+id/textView_author_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="8dp"
android:singleLine="true"
android:text="@string/author"
android:textAppearance="?textAppearanceTitleSmall"
app:layout_constraintStart_toStartOf="@id/card_details"
app:layout_constraintTop_toBottomOf="@id/textView_source_label" />
<TextView
android:id="@+id/textView_author"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:background="@drawable/custom_selectable_item_background"
android:padding="4dp"
android:singleLine="true"
android:textAppearance="?textAppearanceBodyMedium"
app:layout_constraintBaseline_toBaselineOf="@id/textView_author_label"
app:layout_constraintEnd_toEndOf="@id/card_details"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@id/barrier_table"
tools:text="Author name" />
<TextView
android:id="@+id/textView_rating_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="8dp"
android:singleLine="true"
android:text="@string/rating"
android:textAppearance="?textAppearanceTitleSmall"
app:layout_constraintStart_toStartOf="@id/card_details"
app:layout_constraintTop_toBottomOf="@id/textView_author_label" />
<RatingBar
android:id="@+id/ratingBar_rating"
style="?ratingBarStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_normal"
android:layout_marginEnd="@dimen/screen_padding"
android:isIndicator="true"
android:max="1"
android:numStars="5"
android:stepSize="0.5"
android:textAppearance="?textAppearanceBodyMedium"
app:layout_constraintBottom_toBottomOf="@id/textView_rating_label"
app:layout_constraintEnd_toEndOf="@id/card_details"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@id/barrier_table"
app:layout_constraintTop_toTopOf="@id/textView_rating_label"
tools:text="Author name" />
<TextView
android:id="@+id/textView_state_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="8dp"
android:singleLine="true"
android:text="@string/state"
android:textAppearance="?textAppearanceTitleSmall"
app:layout_constraintStart_toStartOf="@id/card_details"
app:layout_constraintTop_toBottomOf="@id/textView_rating_label" />
<TextView
android:id="@+id/textView_state"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_normal"
android:layout_marginEnd="@dimen/screen_padding"
android:singleLine="true"
android:textAppearance="?textAppearanceBodyMedium"
app:layout_constraintBaseline_toBaselineOf="@id/textView_state_label"
app:layout_constraintEnd_toEndOf="@id/card_details"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@id/barrier_table"
tools:text="Ongoing" />
<TextView
android:id="@+id/textView_chapters_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="8dp"
android:singleLine="true"
android:text="@string/chapters"
android:textAppearance="?textAppearanceTitleSmall"
app:layout_constraintStart_toStartOf="@id/card_details"
app:layout_constraintTop_toBottomOf="@id/textView_state_label" />
<TextView
android:id="@+id/textView_chapters"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_normal"
android:layout_marginEnd="@dimen/screen_padding"
android:singleLine="true"
android:textAppearance="?textAppearanceBodyMedium"
app:layout_constraintBaseline_toBaselineOf="@id/textView_chapters_label"
app:layout_constraintEnd_toEndOf="@id/card_details"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@id/barrier_table"
tools:text="10 of 50" />
<TextView
android:id="@+id/textView_local_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="8dp"
android:singleLine="true"
android:text="@string/on_device"
android:textAppearance="?textAppearanceTitleSmall"
app:layout_constraintStart_toStartOf="@id/card_details"
app:layout_constraintTop_toBottomOf="@id/textView_chapters_label" />
<TextView
android:id="@+id/textView_local"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:background="@drawable/custom_selectable_item_background"
android:padding="4dp"
android:singleLine="true"
android:textAppearance="?textAppearanceBodyMedium"
app:layout_constraintBaseline_toBaselineOf="@id/textView_local_label"
app:layout_constraintEnd_toEndOf="@id/card_details"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@id/barrier_table"
tools:text="25 Mb" />
<TextView
android:id="@+id/textView_progress_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="8dp"
android:singleLine="true"
android:text="@string/progress"
android:textAppearance="?textAppearanceTitleSmall"
app:layout_constraintStart_toStartOf="@id/card_details"
app:layout_constraintTop_toBottomOf="@id/textView_local_label" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress"
style="@style/Widget.Material3.LinearProgressIndicator"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_normal"
android:layout_marginEnd="12dp"
android:indeterminate="false"
android:max="100"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="@id/textView_progress_label"
app:layout_constraintEnd_toStartOf="@id/textView_progress"
app:layout_constraintStart_toEndOf="@id/barrier_table"
app:layout_constraintTop_toTopOf="@id/textView_progress_label"
tools:progress="12" />
<TextView
android:id="@+id/textView_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/screen_padding"
android:singleLine="true"
android:textAppearance="?textAppearanceBodyMedium"
app:layout_constraintBaseline_toBaselineOf="@id/textView_progress_label"
app:layout_constraintEnd_toEndOf="@id/card_details"
tools:text="40%" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier_table"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:barrierDirection="end"
app:constraint_referenced_ids="textView_source_label,textView_author_label,textView_rating_label,textView_state_label,textView_progress_label,textView_chapters_label,textView_local_label" />
</merge>

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
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"
style="?materialCardViewOutlinedStyle"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="12dp">
<TextView
android:id="@+id/textView_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="12dp"
android:textAppearance="?textAppearanceTitleSmall"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="46 chapters" />
<TextView
android:id="@+id/textView_percent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:textAppearance="?textAppearanceSubtitle1"
app:layout_constraintBaseline_toBaselineOf="@id/textView_title"
app:layout_constraintEnd_toEndOf="parent"
tools:text="12%" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress"
style="@style/Widget.Material3.LinearProgressIndicator"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="12dp"
android:layout_marginTop="12dp"
android:indeterminate="false"
android:max="100"
android:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textView_title"
tools:progress="12" />
<TextView
android:id="@+id/textView_secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="12dp"
android:textAppearance="?textAppearanceBodySmall"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/progress"
tools:text="@string/incognito_mode" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

@ -17,16 +17,57 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:baselineAligned="false"
android:gravity="center_vertical"
android:orientation="horizontal">
<com.google.android.material.tabs.TabLayout <com.google.android.material.tabs.TabLayout
android:id="@+id/tabs" android:id="@+id/tabs"
style="@style/Widget.Material3.TabLayout.Secondary" style="@style/Widget.Material3.TabLayout.Secondary"
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="start" android:layout_gravity="start"
android:layout_weight="1"
android:background="@null" android:background="@null"
app:tabGravity="start" app:tabGravity="start"
app:tabMode="scrollable" app:tabIndicator="@drawable/bg_tab_pill"
app:tabUnboundedRipple="false" /> app:tabIndicatorAnimationMode="fade"
app:tabIndicatorColor="?colorSurfaceDim"
app:tabIndicatorFullWidth="true"
app:tabIndicatorGravity="stretch"
app:tabInlineLabel="true"
app:tabMinWidth="0dp"
app:tabMode="scrollable" />
<com.google.android.material.button.MaterialSplitButton
android:id="@+id/split_button_read"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end">
<Button
android:id="@+id/button_read"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/read" />
<Button
android:id="@+id/button_read_menu"
style="?attr/materialSplitButtonIconFilledStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:minWidth="48dp"
app:icon="@drawable/m3_split_button_chevron_avd"
app:toggleCheckedStateOnClick="false" />
</com.google.android.material.button.MaterialSplitButton>
</LinearLayout>
</com.google.android.material.appbar.MaterialToolbar> </com.google.android.material.appbar.MaterialToolbar>

@ -12,10 +12,11 @@
<item <item
android:id="@+id/action_save" android:id="@+id/action_save"
android:icon="@drawable/ic_download"
android:orderInCategory="40" android:orderInCategory="40"
android:title="@string/download" android:title="@string/download"
android:visible="false" android:visible="false"
app:showAsAction="never" /> app:showAsAction="ifRoom" />
<item <item
android:id="@+id/action_delete" android:id="@+id/action_delete"

@ -132,6 +132,7 @@
<attr name="strokeColor" /> <attr name="strokeColor" />
<attr name="strokeWidth" /> <attr name="strokeWidth" />
<attr name="cornerSize" /> <attr name="cornerSize" />
<attr name="drawableSize" />
</declare-styleable> </declare-styleable>
<declare-styleable name="NestedRecyclerView"> <declare-styleable name="NestedRecyclerView">

@ -34,7 +34,7 @@
<dimen name="chapter_grid_width">80dp</dimen> <dimen name="chapter_grid_width">80dp</dimen>
<dimen name="side_card_offset">8dp</dimen> <dimen name="side_card_offset">8dp</dimen>
<dimen name="webtoon_pages_gap">24dp</dimen> <dimen name="webtoon_pages_gap">24dp</dimen>
<dimen name="details_bs_peek_height">92dp</dimen> <dimen name="details_bs_peek_height">120dp</dimen>
<dimen name="spinner_height">56dp</dimen> <dimen name="spinner_height">56dp</dimen>
<dimen name="search_suggestions_manga_height">142dp</dimen> <dimen name="search_suggestions_manga_height">142dp</dimen>

@ -769,4 +769,7 @@
<string name="handle_links_summary">Handle manga links from external applications (e.g. web browser). You may also need to enable it manually in the application\'s system settings</string> <string name="handle_links_summary">Handle manga links from external applications (e.g. web browser). You may also need to enable it manually in the application\'s system settings</string>
<string name="email">Email</string> <string name="email">Email</string>
<string name="captcha_required_message">This source requires solving a captcha to continue</string> <string name="captcha_required_message">This source requires solving a captcha to continue</string>
<string name="author">Author</string>
<string name="rating">Rating</string>
<string name="source">Source</string>
</resources> </resources>

@ -328,4 +328,10 @@
<item name="cornerSize">12dp</item> <item name="cornerSize">12dp</item>
</style> </style>
<style name="FaviconDrawable.Chip">
<item name="strokeWidth">1px</item>
<item name="cornerSize">@dimen/chip_icon_corner</item>
<item name="drawableSize">@dimen/m3_chip_icon_size</item>
</style>
</resources> </resources>

@ -26,7 +26,7 @@ ksp = "2.0.21-1.0.28"
leakcanary = "3.0-alpha-8" leakcanary = "3.0-alpha-8"
lifecycle = "2.8.7" lifecycle = "2.8.7"
markwon = "4.6.2" markwon = "4.6.2"
material = "1.12.0" material = "1.13.0-alpha08"
moshi = "1.15.1" moshi = "1.15.1"
okhttp = "4.12.0" okhttp = "4.12.0"
okio = "3.9.1" okio = "3.9.1"

Loading…
Cancel
Save