From 8c79df3d3576f29fbc88774fd6f859925e82c54f Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 25 Nov 2024 09:32:36 +0200 Subject: [PATCH] Details ui updates --- app/build.gradle | 4 +- .../core/ui/image/AnimatedFaviconDrawable.kt | 18 ++ .../kotatsu/core/ui/image/FaviconDrawable.kt | 24 ++ .../kotatsu/core/ui/image/TextViewTarget.kt | 41 +++ .../kotatsu/details/ui/DetailsActivity.kt | 193 +++++++------- .../kotatsu/details/ui/ReadButtonDelegate.kt | 147 +++++++++++ .../details/ui/pager/ChaptersPagesAdapter.kt | 16 +- .../details/ui/pager/ChaptersPagesSheet.kt | 7 + app/src/main/res/drawable/bg_tab_pill.xml | 18 ++ .../layout-w600dp-land/activity_details.xml | 93 +------ app/src/main/res/layout/activity_details.xml | 123 +-------- .../main/res/layout/layout_details_chips.xml | 76 ------ .../main/res/layout/layout_details_table.xml | 235 ++++++++++++++++++ .../res/layout/layout_reading_progress.xml | 64 +++++ .../main/res/layout/sheet_chapters_pages.xml | 57 ++++- app/src/main/res/menu/opt_details.xml | 3 +- app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/dimens.xml | 2 +- app/src/main/res/values/strings.xml | 3 + app/src/main/res/values/styles.xml | 6 + gradle/libs.versions.toml | 2 +- 21 files changed, 746 insertions(+), 387 deletions(-) create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/TextViewTarget.kt create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ReadButtonDelegate.kt create mode 100644 app/src/main/res/drawable/bg_tab_pill.xml delete mode 100644 app/src/main/res/layout/layout_details_chips.xml create mode 100644 app/src/main/res/layout/layout_details_table.xml create mode 100644 app/src/main/res/layout/layout_reading_progress.xml diff --git a/app/build.gradle b/app/build.gradle index c2d668f03..e80189f59 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,8 +18,8 @@ android { applicationId 'org.koitharu.kotatsu' minSdk = 21 targetSdk = 35 - versionCode = 693 - versionName = '7.7.1' + versionCode = 700 + versionName = '8.0-a1' generatedDensities = [] testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner' ksp { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/AnimatedFaviconDrawable.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/AnimatedFaviconDrawable.kt index 24e15150c..39d1405f4 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/AnimatedFaviconDrawable.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/AnimatedFaviconDrawable.kt @@ -6,11 +6,17 @@ import android.graphics.Canvas import android.graphics.drawable.Animatable import androidx.annotation.StyleRes 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.color.MaterialColors 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.ext.getAnimationDuration +import org.koitharu.kotatsu.core.util.ext.mangaSourceKey import kotlin.math.abs class AnimatedFaviconDrawable( @@ -69,4 +75,16 @@ class AnimatedFaviconDrawable( colorForeground = ArgbEvaluatorCompat.getInstance() .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() + } + } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/FaviconDrawable.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/FaviconDrawable.kt index 5beacf886..108b7175d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/FaviconDrawable.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/FaviconDrawable.kt @@ -13,9 +13,15 @@ import android.graphics.drawable.Drawable import androidx.annotation.StyleRes import androidx.core.content.withStyledAttributes 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 org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.model.getTitle import org.koitharu.kotatsu.core.util.KotatsuColors +import org.koitharu.kotatsu.core.util.ext.mangaSourceKey open class FaviconDrawable( context: Context, @@ -29,6 +35,7 @@ open class FaviconDrawable( private var colorStroke = Color.LTGRAY private val letter = name.take(1).uppercase() private var cornerSize = 0f + private var intrinsicSize = -1 private val textBounds = Rect() private val tempRect = Rect() private val boundsF = RectF() @@ -40,6 +47,7 @@ open class FaviconDrawable( colorStroke = getColor(R.styleable.FaviconFallbackDrawable_strokeColor, colorStroke) cornerSize = getDimension(R.styleable.FaviconFallbackDrawable_cornerSize, cornerSize) paint.strokeWidth = getDimension(R.styleable.FaviconFallbackDrawable_strokeWidth, 0f) * 2f + intrinsicSize = getDimensionPixelSize(R.styleable.FaviconFallbackDrawable_drawableSize, intrinsicSize) } paint.textAlign = Paint.Align.CENTER paint.isFakeBoldText = true @@ -75,6 +83,10 @@ open class FaviconDrawable( paint.colorFilter = colorFilter } + override fun getIntrinsicWidth(): Int = intrinsicSize + + override fun getIntrinsicHeight(): Int = intrinsicSize + @Suppress("DeprecatedCallableAddReplaceWith") @Deprecated("Deprecated in Java") override fun getOpacity() = PixelFormat.TRANSPARENT @@ -103,4 +115,16 @@ open class FaviconDrawable( paint.getTextBounds(text, 0, text.length, tempRect) 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() + } + } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/TextViewTarget.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/TextViewTarget.kt new file mode 100644 index 000000000..e2befcad5 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/TextViewTarget.kt @@ -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() { + + 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], + ) + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt index 720d33f28..577ffa907 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt @@ -9,6 +9,7 @@ import android.text.style.ForegroundColorSpan import android.text.style.ImageSpan import android.text.style.RelativeSizeSpan import android.transition.TransitionManager +import android.view.Gravity import android.view.Menu import android.view.MenuItem import android.view.View @@ -37,6 +38,7 @@ import coil3.request.lifecycle import coil3.request.placeholder import coil3.request.target import coil3.request.transformations +import coil3.size.Precision import coil3.size.Scale import coil3.transform.RoundedCornersTransformation 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.UnknownMangaSource 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.titleResId 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.BaseListAdapter 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.FaviconDrawable +import org.koitharu.kotatsu.core.ui.image.TextViewTarget import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.sheet.BottomSheetCollapseCallback 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.scaleUpActivityOptionsOf 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.databinding.ActivityDetailsBinding +import org.koitharu.kotatsu.databinding.LayoutDetailsTableBinding import org.koitharu.kotatsu.details.data.MangaDetails import org.koitharu.kotatsu.details.data.ReadingTime 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.MangaListFilter 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.scrobbling.common.domain.model.ScrobblingInfo import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet import org.koitharu.kotatsu.search.ui.MangaListActivity -import org.koitharu.kotatsu.stats.ui.sheet.MangaStatsSheet import javax.inject.Inject +import kotlin.math.roundToInt import com.google.android.material.R as materialR @AndroidEntryPoint @@ -140,30 +141,24 @@ class DetailsActivity : private val viewModel: DetailsViewModel by viewModels() private lateinit var menuProvider: DetailsMenuProvider + private lateinit var infoBinding: LayoutDetailsTableBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(ActivityDetailsBinding.inflate(layoutInflater)) + infoBinding = LayoutDetailsTableBinding.bind(viewBinding.root) supportActionBar?.run { setDisplayHomeAsUpEnabled(true) setDisplayShowTitleEnabled(false) } - viewBinding.buttonRead.setOnClickListener(this) - viewBinding.buttonRead.setOnLongClickListener(this) - viewBinding.buttonRead.setOnContextClickListenerCompat(this) - viewBinding.buttonDownload?.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.chipFavorite.setOnClickListener(this) + infoBinding.textViewLocal.setOnClickListener(this) + infoBinding.textViewAuthor.setOnClickListener(this) + infoBinding.textViewSource.setOnClickListener(this) viewBinding.imageViewCover.setOnClickListener(this) viewBinding.buttonDescriptionMore.setOnClickListener(this) viewBinding.buttonScrobblingMore.setOnClickListener(this) viewBinding.buttonRelatedMore.setOnClickListener(this) - viewBinding.infoLayout.chipSource.setOnClickListener(this) - viewBinding.infoLayout.chipSize.setOnClickListener(this) viewBinding.textViewDescription.addOnLayoutChangeListener(this) viewBinding.swipeRefreshLayout.setOnRefreshListener(this) viewBinding.textViewDescription.viewTreeObserver.addOnDrawListener(this) @@ -191,17 +186,17 @@ class DetailsActivity : viewModel.localSize.observe(this, ::onLocalSizeChanged) viewModel.relatedManga.observe(this, ::onRelatedMangaChanged) viewModel.readingTime.observe(this, ::onReadingTimeChanged) - viewModel.selectedBranch.observe(this) { - viewBinding.infoLayout.chipBranch.text = it.ifNullOrEmpty { getString(R.string.system_default) } - } + // viewModel.selectedBranch.observe(this) { + // viewBinding.infoLayout?.chipBranch?.text = it.ifNullOrEmpty { getString(R.string.system_default) } + // } viewModel.favouriteCategories.observe(this, ::onFavoritesChanged) val menuInvalidator = MenuInvalidator(this) viewModel.isStatsAvailable.observe(this, menuInvalidator) viewModel.remoteManga.observe(this, menuInvalidator) - viewModel.branches.observe(this) { - viewBinding.infoLayout.chipBranch.isVisible = it.size > 1 || !it.firstOrNull()?.name.isNullOrEmpty() - viewBinding.infoLayout.chipBranch.isCloseIconVisible = it.size > 1 - } + // viewModel.branches.observe(this) { + // viewBinding.infoLayout?.chipBranch?.isVisible = it.size > 1 || !it.firstOrNull()?.name.isNullOrEmpty() + // viewBinding.infoLayout?.chipBranch?.isCloseIconVisible = it.size > 1 + // } viewModel.chapters.observe(this, PrefetchObserver(this)) viewModel.onDownloadStarted .filterNot { ChaptersPagesSheet.isShown(supportFragmentManager) } @@ -221,14 +216,9 @@ class DetailsActivity : override fun onClick(v: View) { when (v.id) { - R.id.button_read -> openReader(isIncognitoMode = false) - R.id.chip_branch -> showBranchPopupMenu(v) - R.id.button_download -> { - val manga = viewModel.manga.value ?: return - DownloadDialogFragment.show(supportFragmentManager, listOf(manga)) - } + // R.id.chip_branch -> showBranchPopupMenu(v) - R.id.chip_author -> { + R.id.textView_author -> { val manga = viewModel.manga.value ?: return startActivity( MangaListActivity.newIntent( @@ -239,7 +229,7 @@ class DetailsActivity : ) } - R.id.chip_source -> { + R.id.textView_source -> { val manga = viewModel.manga.value ?: return startActivity( MangaListActivity.newIntent( @@ -250,7 +240,7 @@ class DetailsActivity : ) } - R.id.chip_size -> { + R.id.textView_local -> { val manga = viewModel.manga.value ?: return LocalInfoDialog.show(supportFragmentManager, manga) } @@ -260,14 +250,14 @@ class DetailsActivity : FavoriteSheet.show(supportFragmentManager, manga) } - R.id.chip_time -> { - if (viewModel.isStatsAvailable.value) { - val manga = viewModel.manga.value ?: return - MangaStatsSheet.show(supportFragmentManager, manga) - } else { - // TODO - } - } + // R.id.chip_time -> { + // if (viewModel.isStatsAvailable.value) { + // val manga = viewModel.manga.value ?: return + // MangaStatsSheet.show(supportFragmentManager, manga) + // } else { + // // TODO + // } + // } R.id.imageView_cover -> { val manga = viewModel.manga.value ?: return @@ -378,7 +368,7 @@ class DetailsActivity : } private fun onFavoritesChanged(categories: Set) { - 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.text = if (categories.isEmpty()) { getString(R.string.add_to_favourites) @@ -388,17 +378,18 @@ class DetailsActivity : } private fun onReadingTimeChanged(time: ReadingTime?) { - val chip = viewBinding.infoLayout.chipTime - chip.textAndVisible = time?.formatShort(chip.resources) + // TODO + // chip.textAndVisible = time?.formatShort(chip.resources) } private fun onLocalSizeChanged(size: Long) { - val chip = viewBinding.infoLayout.chipSize if (size == 0L) { - chip.isVisible = false + infoBinding.textViewLocal.isVisible = false + infoBinding.textViewLocalLabel.isVisible = false } else { - chip.text = FileSize.BYTES.format(chip.context, size) - chip.isVisible = true + infoBinding.textViewLocal.text = FileSize.BYTES.format(this, size) + infoBinding.textViewLocal.isVisible = true + infoBinding.textViewLocalLabel.isVisible = true } } @@ -442,62 +433,59 @@ class DetailsActivity : } private fun onMangaUpdated(details: MangaDetails) { + val manga = details.toManga() + loadCover(manga) with(viewBinding) { - val manga = details.toManga() - // Main - loadCover(manga) textViewTitle.text = manga.title 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) { - ratingBar.rating = manga.rating * ratingBar.numStars - ratingBar.isVisible = true + ratingBarRating.rating = manga.rating * ratingBarRating.numStars + ratingBarRating.isVisible = true + textViewRatingLabel.isVisible = true } else { - ratingBar.isVisible = false + ratingBarRating.isVisible = false + textViewRatingLabel.isVisible = false } - manga.state?.let { state -> textViewState.textAndVisible = resources.getString(state.titleResId) - imageViewState.setImageResource(state.iconResId) - imageViewState.isVisible = true + textViewStateLabel.isVisible = textViewState.isVisible } ?: run { textViewState.isVisible = false - imageViewState.isVisible = false + textViewStateLabel.isVisible = false } if (manga.source == LocalMangaSource || manga.source == UnknownMangaSource) { - infoLayout.chipSource.isVisible = false + textViewSource.isVisible = false + textViewSourceLabel.isVisible = false } else { - infoLayout.chipSource.text = manga.source.getTitle(this@DetailsActivity) - infoLayout.chipSource.isVisible = true + textViewSource.textAndVisible = manga.source.getTitle(this@DetailsActivity) + textViewSourceLabel.isVisible = textViewSource.isVisible == true } - - 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) - .data(manga.source.faviconUri()) - .lifecycle(this@DetailsActivity) - .crossfade(false) - .size(resources.getDimensionPixelSize(materialR.dimen.m3_chip_icon_size)) - .target(ChipIconTarget(chip)) - .placeholder(R.drawable.ic_web) - .fallback(R.drawable.ic_web) - .error(R.drawable.ic_web) - .mangaSourceExtra(manga.source) - .transformations(RoundedCornersTransformation(resources.getDimension(R.dimen.chip_icon_corner))) - .allowRgb565(true) - .enqueueWith(coil) - } - - title = manga.title - invalidateOptionsMenu() + val faviconPlaceholderFactory = FaviconDrawable.Factory(R.style.FaviconDrawable_Chip) + ImageRequest.Builder(this@DetailsActivity) + .data(manga.source.faviconUri()) + .lifecycle(this@DetailsActivity) + .crossfade(false) + .precision(Precision.EXACT) + .size(resources.getDimensionPixelSize(materialR.dimen.m3_chip_icon_size)) + .target(TextViewTarget(textViewSource, Gravity.START)) + .placeholder(faviconPlaceholderFactory) + .error(faviconPlaceholderFactory) + .fallback(faviconPlaceholderFactory) + .mangaSourceExtra(manga.source) + .transformations(RoundedCornersTransformation(resources.getDimension(R.dimen.chip_icon_corner))) + .allowRgb565(true) + .enqueueWith(coil) } + bindTags(manga) + title = manga.title + invalidateOptionsMenu() } private fun onMangaRemoved(manga: Manga) { @@ -527,22 +515,28 @@ class DetailsActivity : } } - private fun onHistoryChanged(info: HistoryInfo, isLoading: Boolean) = with(viewBinding) { - buttonRead.setTitle(if (info.canContinue) R.string._continue else R.string.read) - buttonRead.subtitle = when { - isLoading -> getString(R.string.loading_) - info.isIncognitoMode -> getString(R.string.incognito_mode) - info.isChapterMissing -> getString(R.string.chapter_is_missing) + private fun onHistoryChanged(info: HistoryInfo, isLoading: Boolean) = with(infoBinding) { + textViewChapters.textAndVisible = when { + isLoading -> null 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 == -1 -> getString(R.string.error_occurred) else -> resources.getQuantityString(R.plurals.chapters, info.totalChapters, info.totalChapters) } - val isFirstCall = buttonRead.tag == null - buttonRead.tag = Unit - buttonRead.setProgress(info.percent.coerceIn(0f, 1f), !isFirstCall) - buttonDownload?.isEnabled = info.isValid && info.canDownload - buttonRead.isEnabled = info.isValid + textViewProgress.textAndVisible = if (info.percent <= 0f) { + null + } else { + getString(R.string.percent_string_pattern, (info.percent * 100f).toInt().toString()) + } + 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) { @@ -663,7 +657,6 @@ class DetailsActivity : companion object { private const val FAV_LABEL_LIMIT = 16 - private const val AUTHOR_LABEL_LIMIT = 16 fun newIntent(context: Context, manga: Manga): Intent { return Intent(context, DetailsActivity::class.java) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ReadButtonDelegate.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ReadButtonDelegate.kt new file mode 100644 index 000000000..19ba6ab38 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ReadButtonDelegate.kt @@ -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) + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesAdapter.kt index 34a98672f..3b4cfd729 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesAdapter.kt @@ -25,13 +25,21 @@ class ChaptersPagesAdapter( } override fun onConfigureTab(tab: TabLayout.Tab, position: Int) { - tab.setText( + tab.setIcon( when (position) { - 0 -> R.string.chapters - 1 -> if (isPagesTabEnabled) R.string.pages else R.string.bookmarks - 2 -> R.string.bookmarks + 0 -> R.drawable.ic_list + 1 -> if (isPagesTabEnabled) R.drawable.ic_grid else R.drawable.ic_bookmark + 2 -> R.drawable.ic_bookmark 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 + // }, + // ) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesSheet.kt index 9aba33af1..87a3b36d6 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesSheet.kt @@ -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.withArgs 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 javax.inject.Inject @@ -54,6 +56,9 @@ class ChaptersPagesSheet : BaseAdaptiveSheet(), Actio if (!adapter.isPagesTabEnabled) { defaultTab = (defaultTab - 1).coerceAtLeast(TAB_CHAPTERS) } + (viewModel as? DetailsViewModel)?.let { dvm -> + ReadButtonDelegate(binding.splitButtonRead, dvm).attach(viewLifecycleOwner) + } binding.pager.offscreenPageLimit = adapter.itemCount binding.pager.recyclerView?.isNestedScrollingEnabled = false binding.pager.adapter = adapter @@ -88,6 +93,8 @@ class ChaptersPagesSheet : BaseAdaptiveSheet(), Actio val binding = viewBinding ?: return val isActionModeStarted = actionModeDelegate?.isActionModeStarted == true binding.toolbar.menuView?.isVisible = newState != STATE_COLLAPSED && !isActionModeStarted + binding.splitButtonRead.isVisible = newState == STATE_COLLAPSED && !isActionModeStarted + && viewModel is DetailsViewModel } override fun onActionModeStarted(mode: ActionMode) { diff --git a/app/src/main/res/drawable/bg_tab_pill.xml b/app/src/main/res/drawable/bg_tab_pill.xml new file mode 100644 index 000000000..246f0c385 --- /dev/null +++ b/app/src/main/res/drawable/bg_tab_pill.xml @@ -0,0 +1,18 @@ + + + + + + + + + diff --git a/app/src/main/res/layout-w600dp-land/activity_details.xml b/app/src/main/res/layout-w600dp-land/activity_details.xml index d90ca8c41..d69a25acd 100644 --- a/app/src/main/res/layout-w600dp-land/activity_details.xml +++ b/app/src/main/res/layout-w600dp-land/activity_details.xml @@ -120,56 +120,17 @@ app:layout_constraintTop_toBottomOf="@id/textView_title" tools:text="@tools:sample/lorem[12]" /> - - - - - + tools:text="@string/add_to_favourites" /> + app:constraint_referenced_ids="imageView_cover,chip_favorite" /> - - - + + app:layout_constraintTop_toBottomOf="@id/textView_progress_label" />