UI improvements and author search support

This commit is contained in:
Koitharu
2025-02-18 19:26:37 +02:00
parent 47a22064a5
commit 151777cf61
32 changed files with 336 additions and 266 deletions

View File

@@ -4,6 +4,7 @@ import android.text.style.ForegroundColorSpan
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.text.buildSpannedString import androidx.core.text.buildSpannedString
import androidx.core.text.inSpans import androidx.core.text.inSpans
import androidx.core.view.isVisible
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil3.ImageLoader import coil3.ImageLoader
import coil3.request.ImageRequest import coil3.request.ImageRequest
@@ -51,7 +52,13 @@ fun alternativeAD(
binding.chipSource.setOnClickListener(clickListener) binding.chipSource.setOnClickListener(clickListener)
bind { payloads -> bind { payloads ->
binding.textViewTitle.text = item.manga.title binding.textViewTitle.text = item.mangaModel.title
with(binding.iconsView) {
clearIcons()
if (item.mangaModel.isSaved) addIcon(R.drawable.ic_storage)
if (item.mangaModel.isFavorite) addIcon(R.drawable.ic_heart_outline)
isVisible = iconsCount > 0
}
binding.textViewSubtitle.text = buildSpannedString { binding.textViewSubtitle.text = buildSpannedString {
if (item.chaptersCount > 0) { if (item.chaptersCount > 0) {
append(context.resources.getQuantityString(R.plurals.chapters, item.chaptersCount, item.chaptersCount)) append(context.resources.getQuantityString(R.plurals.chapters, item.chaptersCount, item.chaptersCount))
@@ -70,7 +77,7 @@ fun alternativeAD(
} }
} }
} }
binding.progressView.setProgress(item.progress, ListModelDiffCallback.PAYLOAD_PROGRESS_CHANGED in payloads) binding.progressView.setProgress(item.mangaModel.progress, ListModelDiffCallback.PAYLOAD_PROGRESS_CHANGED in payloads)
binding.chipSource.also { chip -> binding.chipSource.also { chip ->
chip.text = item.manga.source.getTitle(chip.context) chip.text = item.manga.source.getTitle(chip.context)
ImageRequest.Builder(context) ImageRequest.Builder(context)

View File

@@ -15,17 +15,17 @@ import org.koitharu.kotatsu.core.model.chaptersCount
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.nav.AppRouter import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.require import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.list.domain.MangaListMapper
import org.koitharu.kotatsu.list.domain.ReadingProgress
import org.koitharu.kotatsu.list.ui.model.EmptyState import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingFooter import org.koitharu.kotatsu.list.ui.model.LoadingFooter
import org.koitharu.kotatsu.list.ui.model.LoadingState import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.list.ui.model.MangaGridModel
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import javax.inject.Inject import javax.inject.Inject
@@ -36,8 +36,7 @@ class AlternativesViewModel @Inject constructor(
private val mangaRepositoryFactory: MangaRepository.Factory, private val mangaRepositoryFactory: MangaRepository.Factory,
private val alternativesUseCase: AlternativesUseCase, private val alternativesUseCase: AlternativesUseCase,
private val migrateUseCase: MigrateUseCase, private val migrateUseCase: MigrateUseCase,
private val historyRepository: HistoryRepository, private val mangaListMapper: MangaListMapper,
private val settings: AppSettings,
) : BaseViewModel() { ) : BaseViewModel() {
val manga = savedStateHandle.require<ParcelableManga>(AppRouter.KEY_MANGA).manga val manga = savedStateHandle.require<ParcelableManga>(AppRouter.KEY_MANGA).manga
@@ -55,8 +54,7 @@ class AlternativesViewModel @Inject constructor(
alternativesUseCase(ref) alternativesUseCase(ref)
.map { .map {
MangaAlternativeModel( MangaAlternativeModel(
manga = it, mangaModel = mangaListMapper.toListModel(it, ListMode.GRID) as MangaGridModel,
progress = getProgress(it.id),
referenceChapters = refCount, referenceChapters = refCount,
) )
}.runningFold<MangaAlternativeModel, List<ListModel>>(listOf(LoadingState)) { acc, item -> }.runningFold<MangaAlternativeModel, List<ListModel>>(listOf(LoadingState)) { acc, item ->
@@ -88,8 +86,4 @@ class AlternativesViewModel @Inject constructor(
onMigrated.call(target) onMigrated.call(target)
} }
} }
private suspend fun getProgress(mangaId: Long): ReadingProgress? {
return historyRepository.getProgress(mangaId, settings.progressIndicatorMode)
}
} }

View File

@@ -1,16 +1,18 @@
package org.koitharu.kotatsu.alternatives.ui package org.koitharu.kotatsu.alternatives.ui
import org.koitharu.kotatsu.core.model.chaptersCount import org.koitharu.kotatsu.core.model.chaptersCount
import org.koitharu.kotatsu.list.domain.ReadingProgress
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.MangaGridModel
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
data class MangaAlternativeModel( data class MangaAlternativeModel(
val manga: Manga, val mangaModel: MangaGridModel,
val progress: ReadingProgress?,
private val referenceChapters: Int, private val referenceChapters: Int,
) : ListModel { ) : ListModel {
val manga: Manga
get() = mangaModel.manga
val chaptersCount = manga.chaptersCount() val chaptersCount = manga.chaptersCount()
val chaptersDiff: Int val chaptersDiff: Int
@@ -19,4 +21,10 @@ data class MangaAlternativeModel(
override fun areItemsTheSame(other: ListModel): Boolean { override fun areItemsTheSame(other: ListModel): Boolean {
return other is MangaAlternativeModel && other.manga.id == manga.id return other is MangaAlternativeModel && other.manga.id == manga.id
} }
override fun getChangePayload(previousState: ListModel): Any? = if (previousState is MangaAlternativeModel) {
mangaModel.getChangePayload(previousState.mangaModel)
} else {
null
}
} }

View File

@@ -1,183 +0,0 @@
package org.koitharu.kotatsu.core.ui.widgets
import android.animation.ValueAnimator
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Outline
import android.graphics.Paint
import android.util.AttributeSet
import android.view.Gravity
import android.view.View
import android.view.ViewOutlineProvider
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.appcompat.widget.LinearLayoutCompat
import androidx.core.content.withStyledAttributes
import androidx.core.graphics.ColorUtils
import androidx.core.view.children
import androidx.core.widget.TextViewCompat
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
import org.koitharu.kotatsu.core.util.ext.getThemeColorStateList
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
import org.koitharu.kotatsu.core.util.ext.resolveDp
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
import org.koitharu.kotatsu.core.util.ext.textAndVisible
import com.google.android.material.R as materialR
@Deprecated("")
class ProgressButton @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : LinearLayoutCompat(context, attrs, defStyleAttr), ValueAnimator.AnimatorUpdateListener {
private val textViewTitle = TextView(context)
private val textViewSubtitle = TextView(context)
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private var progress = 0f
private var targetProgress = 0f
private var colorBase: ColorStateList = ColorStateList.valueOf(Color.TRANSPARENT)
private var colorProgress: ColorStateList = ColorStateList.valueOf(Color.TRANSPARENT)
private var progressAnimator: ValueAnimator? = null
private var colorBaseCurrent = colorProgress.defaultColor
private var colorProgressCurrent = colorProgress.defaultColor
var title: CharSequence?
get() = textViewTitle.textAndVisible
set(value) {
textViewTitle.textAndVisible = value
}
var subtitle: CharSequence?
get() = textViewSubtitle.textAndVisible
set(value) {
textViewSubtitle.textAndVisible = value
}
init {
orientation = VERTICAL
outlineProvider = OutlineProvider()
clipToOutline = true
context.withStyledAttributes(attrs, R.styleable.ProgressButton, defStyleAttr) {
val textAppearanceFallback = androidx.appcompat.R.style.TextAppearance_AppCompat
TextViewCompat.setTextAppearance(
textViewTitle,
getResourceId(R.styleable.ProgressButton_titleTextAppearance, textAppearanceFallback),
)
TextViewCompat.setTextAppearance(
textViewSubtitle,
getResourceId(R.styleable.ProgressButton_subtitleTextAppearance, textAppearanceFallback),
)
textViewTitle.text = getText(R.styleable.ProgressButton_title)
textViewSubtitle.text = getText(R.styleable.ProgressButton_subtitle)
colorBase = getColorStateList(R.styleable.ProgressButton_baseColor)
?: context.getThemeColorStateList(materialR.attr.colorPrimaryContainer) ?: colorBase
colorProgress = getColorStateList(R.styleable.ProgressButton_progressColor)
?: context.getThemeColorStateList(materialR.attr.colorPrimary) ?: colorProgress
getColorStateList(R.styleable.ProgressButton_android_textColor)?.let { colorText ->
textViewTitle.setTextColor(colorText)
textViewSubtitle.setTextColor(colorText)
}
progress = getInt(R.styleable.ProgressButton_android_progress, 0).toFloat() /
getInt(R.styleable.ProgressButton_android_max, 100).toFloat()
}
addView(textViewTitle, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
addView(
textViewSubtitle,
LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).also { lp ->
lp.topMargin = context.resources.resolveDp(2)
},
)
paint.style = Paint.Style.FILL
applyGravity()
setWillNotDraw(false)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawColor(colorBaseCurrent)
if (progress > 0f) {
canvas.drawRect(0f, 0f, width * progress, height.toFloat(), paint)
}
}
override fun drawableStateChanged() {
super.drawableStateChanged()
val state = drawableState
colorBaseCurrent = colorBase.getColorForState(state, colorBase.defaultColor)
colorProgressCurrent = colorProgress.getColorForState(state, colorProgress.defaultColor)
colorProgressCurrent = ColorUtils.setAlphaComponent(colorProgressCurrent, 84 /* 255 * 0.33F */)
paint.color = colorProgressCurrent
}
override fun setGravity(gravity: Int) {
super.setGravity(gravity)
if (childCount != 0) {
applyGravity()
}
}
override fun setEnabled(enabled: Boolean) {
super.setEnabled(enabled)
children.forEach { it.isEnabled = enabled }
}
override fun onAnimationUpdate(animation: ValueAnimator) {
if (animation === progressAnimator) {
progress = animation.animatedValue as Float
invalidate()
}
}
fun setTitle(@StringRes titleResId: Int) {
textViewTitle.setTextAndVisible(titleResId)
}
fun setSubtitle(@StringRes titleResId: Int) {
textViewSubtitle.setTextAndVisible(titleResId)
}
fun setProgress(value: Float, animate: Boolean) {
val prevAnimator = progressAnimator
if (animate && context.isAnimationsEnabled) {
if (value == targetProgress) {
return
}
targetProgress = value
progressAnimator = ValueAnimator.ofFloat(progress, value).apply {
duration = context.getAnimationDuration(android.R.integer.config_mediumAnimTime)
interpolator = AccelerateDecelerateInterpolator()
addUpdateListener(this@ProgressButton)
}
progressAnimator?.start()
} else {
progressAnimator = null
progress = value
targetProgress = value
invalidate()
}
prevAnimator?.cancel()
}
private fun applyGravity() {
val value = (gravity and Gravity.HORIZONTAL_GRAVITY_MASK) or Gravity.CENTER_VERTICAL
textViewTitle.gravity = value
textViewSubtitle.gravity = value
}
private class OutlineProvider : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
outline.setRoundRect(0, 0, view.width, view.height, view.height / 2f)
}
}
}

View File

@@ -92,6 +92,7 @@ import org.koitharu.kotatsu.details.ui.scrobbling.ScrobblingItemDecoration
import org.koitharu.kotatsu.details.ui.scrobbling.ScrollingInfoAdapter import org.koitharu.kotatsu.details.ui.scrobbling.ScrollingInfoAdapter
import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver
import org.koitharu.kotatsu.list.domain.MangaListMapper import org.koitharu.kotatsu.list.domain.MangaListMapper
import org.koitharu.kotatsu.list.domain.ReadingProgress
import org.koitharu.kotatsu.list.ui.adapter.ListItemType import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.adapter.mangaGridItemAD import org.koitharu.kotatsu.list.ui.adapter.mangaGridItemAD
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
@@ -101,6 +102,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
import org.koitharu.kotatsu.search.domain.SearchKind
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.roundToInt import kotlin.math.roundToInt
import com.google.android.material.R as materialR import com.google.android.material.R as materialR
@@ -203,8 +205,8 @@ class DetailsActivity :
override fun onClick(v: View) { override fun onClick(v: View) {
when (v.id) { when (v.id) {
R.id.textView_author -> { R.id.textView_author -> {
val manga = viewModel.manga.value ?: return val author = viewModel.manga.value?.author ?: return
router.openSearch(manga.source, manga.author ?: return) router.openSearch(author, SearchKind.AUTHOR)
} }
R.id.textView_source -> { R.id.textView_source -> {
@@ -484,7 +486,7 @@ class DetailsActivity :
textViewProgress.textAndVisible = if (info.percent <= 0f) { textViewProgress.textAndVisible = if (info.percent <= 0f) {
null null
} else { } else {
val displayPercent = if (info.percent >= 0.999999f) 100 else (info.percent * 100f).toInt() val displayPercent = if (ReadingProgress.isCompleted(info.percent)) 100 else (info.percent * 100f).toInt()
getString(R.string.percent_string_pattern, displayPercent.toString()) getString(R.string.percent_string_pattern, displayPercent.toString())
} }

View File

@@ -287,6 +287,15 @@ class FilterCoordinator @Inject constructor(
} }
} }
fun setAuthor(value: String?) {
currentListFilter.update { oldValue ->
oldValue.copy(
author = value,
query = oldValue.takeQueryIfSupported(),
)
}
}
fun setOriginalLocale(value: Locale?) { fun setOriginalLocale(value: Locale?) {
currentListFilter.update { oldValue -> currentListFilter.update { oldValue ->
oldValue.copy( oldValue.copy(

View File

@@ -60,7 +60,11 @@ class FilterHeaderFragment : BaseFragment<FragmentFilterHeaderBinding>(), ChipsV
override fun onChipCloseClick(chip: Chip, data: Any?) { override fun onChipCloseClick(chip: Chip, data: Any?) {
when (data) { when (data) {
is String -> filter.setQuery(null) is String -> if (data == filter.snapshot().listFilter.author) {
filter.setAuthor(null)
} else {
filter.setQuery(null)
}
is ContentRating -> filter.toggleContentRating(data, false) is ContentRating -> filter.toggleContentRating(data, false)
is Demographic -> filter.toggleDemographic(data, false) is Demographic -> filter.toggleDemographic(data, false)
is ContentType -> filter.toggleContentType(data, false) is ContentType -> filter.toggleContentType(data, false)

View File

@@ -135,6 +135,16 @@ class FilterHeaderProducer @Inject constructor(
), ),
) )
} }
if (!snapshot.author.isNullOrEmpty()) {
result.addFirst(
ChipsView.ChipModel(
title = snapshot.author,
icon = R.drawable.ic_user,
isCloseable = true,
data = snapshot.author,
),
)
}
val hasTags = result.any { it.data is MangaTag } val hasTags = result.any { it.data is MangaTag }
if (hasTags) { if (hasTags) {
result.addLast(moreTagsChip()) result.addLast(moreTagsChip())

View File

@@ -154,7 +154,7 @@ class HistoryRepository @Inject constructor(
suspend fun getProgress(mangaId: Long, mode: ProgressIndicatorMode): ReadingProgress? { suspend fun getProgress(mangaId: Long, mode: ProgressIndicatorMode): ReadingProgress? {
val entity = db.getHistoryDao().find(mangaId) ?: return null val entity = db.getHistoryDao().find(mangaId) ?: return null
val fixedPercent = if (entity.percent >= 0.999999f) 1f else entity.percent val fixedPercent = if (ReadingProgress.isCompleted(entity.percent)) 1f else entity.percent
return ReadingProgress( return ReadingProgress(
percent = fixedPercent, percent = fixedPercent,
totalChapters = entity.chaptersCount, totalChapters = entity.chaptersCount,

View File

@@ -31,6 +31,7 @@ import org.koitharu.kotatsu.list.domain.ListFilterOption
import org.koitharu.kotatsu.list.domain.ListSortOrder import org.koitharu.kotatsu.list.domain.ListSortOrder
import org.koitharu.kotatsu.list.domain.MangaListMapper import org.koitharu.kotatsu.list.domain.MangaListMapper
import org.koitharu.kotatsu.list.domain.QuickFilterListener import org.koitharu.kotatsu.list.domain.QuickFilterListener
import org.koitharu.kotatsu.list.domain.ReadingProgress
import org.koitharu.kotatsu.list.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.EmptyState import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.InfoModel import org.koitharu.kotatsu.list.ui.model.InfoModel
@@ -207,10 +208,10 @@ class HistoryListViewModel @Inject constructor(
ListSortOrder.UNREAD, ListSortOrder.UNREAD,
ListSortOrder.PROGRESS -> ListHeader( ListSortOrder.PROGRESS -> ListHeader(
when (percent) { when {
1f -> R.string.status_completed ReadingProgress.isCompleted(percent) -> R.string.status_completed
in 0f..0.01f -> R.string.status_planned percent in 0f..0.01f -> R.string.status_planned
in 0f..1f -> R.string.status_reading percent in 0f..1f -> R.string.status_reading
else -> R.string.unknown else -> R.string.unknown
}, },
) )

View File

@@ -39,9 +39,10 @@ data class ReadingProgress(
const val PROGRESS_NONE = -1f const val PROGRESS_NONE = -1f
const val PROGRESS_COMPLETED = 1f const val PROGRESS_COMPLETED = 1f
private const val PROGRESS_COMPLETED_THRESHOLD = 0.99999f
fun isValid(percent: Float) = percent in 0f..1f fun isValid(percent: Float) = percent in 0f..1f
fun isCompleted(percent: Float) = percent >= PROGRESS_COMPLETED fun isCompleted(percent: Float) = percent >= PROGRESS_COMPLETED_THRESHOLD
} }
} }

View File

@@ -0,0 +1,21 @@
package org.koitharu.kotatsu.list.ui.adapter
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.databinding.ItemButtonFooterBinding
import org.koitharu.kotatsu.list.ui.model.ButtonFooter
import org.koitharu.kotatsu.list.ui.model.ListModel
fun buttonFooterAD(
listener: ListStateHolderListener,
) = adapterDelegateViewBinding<ButtonFooter, ListModel, ItemButtonFooterBinding>(
{ inflater, parent -> ItemButtonFooterBinding.inflate(inflater, parent, false) },
) {
binding.button.setOnClickListener {
listener.onFooterButtonClick()
}
bind {
binding.button.setText(item.textResId)
}
}

View File

@@ -7,7 +7,7 @@ import org.koitharu.kotatsu.list.ui.model.ErrorFooter
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
fun errorFooterAD( fun errorFooterAD(
listener: MangaListListener?, listener: ListStateHolderListener?,
) = adapterDelegateViewBinding<ErrorFooter, ListModel, ItemErrorFooterBinding>( ) = adapterDelegateViewBinding<ErrorFooter, ListModel, ItemErrorFooterBinding>(
{ inflater, parent -> ItemErrorFooterBinding.inflate(inflater, parent, false) }, { inflater, parent -> ItemErrorFooterBinding.inflate(inflater, parent, false) },
) { ) {

View File

@@ -15,6 +15,7 @@ enum class ListItemType {
MANGA_NESTED_GROUP, MANGA_NESTED_GROUP,
FOOTER_LOADING, FOOTER_LOADING,
FOOTER_ERROR, FOOTER_ERROR,
FOOTER_BUTTON,
STATE_LOADING, STATE_LOADING,
STATE_ERROR, STATE_ERROR,
STATE_EMPTY, STATE_EMPTY,

View File

@@ -7,4 +7,6 @@ interface ListStateHolderListener {
fun onSecondaryErrorActionClick(error: Throwable) = Unit fun onSecondaryErrorActionClick(error: Throwable) = Unit
fun onEmptyActionClick() fun onEmptyActionClick()
fun onFooterButtonClick() = Unit
} }

View File

@@ -27,5 +27,6 @@ open class MangaListAdapter(
addDelegate(ListItemType.QUICK_FILTER, quickFilterAD(listener)) addDelegate(ListItemType.QUICK_FILTER, quickFilterAD(listener))
addDelegate(ListItemType.TIP, tipAD(listener)) addDelegate(ListItemType.TIP, tipAD(listener))
addDelegate(ListItemType.INFO, infoAD()) addDelegate(ListItemType.INFO, infoAD())
addDelegate(ListItemType.FOOTER_BUTTON, buttonFooterAD(listener))
} }
} }

View File

@@ -54,6 +54,7 @@ class TypedListSpacingDecoration(
ListItemType.FOOTER_LOADING, ListItemType.FOOTER_LOADING,
ListItemType.FOOTER_ERROR, ListItemType.FOOTER_ERROR,
ListItemType.FOOTER_BUTTON,
ListItemType.STATE_LOADING, ListItemType.STATE_LOADING,
ListItemType.STATE_ERROR, ListItemType.STATE_ERROR,
ListItemType.STATE_EMPTY, ListItemType.STATE_EMPTY,

View File

@@ -0,0 +1,12 @@
package org.koitharu.kotatsu.list.ui.model
import androidx.annotation.StringRes
data class ButtonFooter(
@StringRes val textResId: Int,
) : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean {
return other is ButtonFooter && textResId == other.textResId
}
}

View File

@@ -121,20 +121,14 @@ class PreviewFragment : BaseFragment<FragmentPreviewBinding>(), View.OnClickList
private fun onFooterUpdated(footer: PreviewViewModel.FooterInfo?) { private fun onFooterUpdated(footer: PreviewViewModel.FooterInfo?) {
with(requireViewBinding()) { with(requireViewBinding()) {
buttonRead.isEnabled = footer != null buttonRead.isEnabled = footer != null
buttonRead.setTitle(if (footer?.isInProgress() == true) R.string._continue else R.string.read) buttonRead.setText(
buttonRead.subtitle = when { when {
footer == null -> getString(R.string.loading_) footer == null -> R.string.loading_
footer.isIncognito -> getString(R.string.incognito_mode) footer.isIncognito == true -> R.string.incognito
footer.currentChapter >= 0 -> getString( footer.isInProgress() == true -> R.string._continue
R.string.chapter_d_of_d, else -> R.string.read
footer.currentChapter + 1, },
footer.totalChapters, )
)
footer.totalChapters == 0 -> getString(R.string.no_chapters)
else -> resources.getQuantityString(R.plurals.chapters, footer.totalChapters, footer.totalChapters)
}
buttonRead.setProgress(footer?.percent?.coerceIn(0f, 1f) ?: 0f, true)
} }
} }

View File

@@ -14,6 +14,7 @@ import android.view.ViewGroup.MarginLayoutParams
import android.view.WindowManager import android.view.WindowManager
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.MenuHost
import androidx.core.view.OnApplyWindowInsetsListener import androidx.core.view.OnApplyWindowInsetsListener
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isGone import androidx.core.view.isGone
@@ -98,6 +99,9 @@ class ReaderActivity :
scrollTimer.isEnabled = value scrollTimer.isEnabled = value
} }
private val secondaryMenuHost: MenuHost
get() = viewBinding.toolbarBottom ?: this
private lateinit var scrollTimer: ScrollTimer private lateinit var scrollTimer: ScrollTimer
private lateinit var pageSaveHelper: PageSaveHelper private lateinit var pageSaveHelper: PageSaveHelper
private lateinit var touchHelper: TapGridDispatcher private lateinit var touchHelper: TapGridDispatcher
@@ -150,7 +154,7 @@ class ReaderActivity :
viewModel.isInfoBarTransparent.observe(this) { viewBinding.infoBar.drawBackground = !it } viewModel.isInfoBarTransparent.observe(this) { viewBinding.infoBar.drawBackground = !it }
viewModel.isInfoBarEnabled.observe(this, ::onReaderBarChanged) viewModel.isInfoBarEnabled.observe(this, ::onReaderBarChanged)
viewModel.isBookmarkAdded.observe(this, MenuInvalidator(this)) viewModel.isBookmarkAdded.observe(this, MenuInvalidator(this))
val bottomMenuInvalidator = MenuInvalidator(viewBinding.toolbarBottom) val bottomMenuInvalidator = MenuInvalidator(secondaryMenuHost)
viewModel.isPagesSheetEnabled.observe(this, bottomMenuInvalidator) viewModel.isPagesSheetEnabled.observe(this, bottomMenuInvalidator)
screenOrientationHelper.observeAutoOrientation().observe(this, bottomMenuInvalidator) screenOrientationHelper.observeAutoOrientation().observe(this, bottomMenuInvalidator)
viewModel.onShowToast.observeEvent(this) { msgId -> viewModel.onShowToast.observeEvent(this) { msgId ->
@@ -165,7 +169,7 @@ class ReaderActivity :
viewBinding.zoomControl.isVisible = it viewBinding.zoomControl.isVisible = it
} }
addMenuProvider(ReaderMenuTopProvider(viewModel)) addMenuProvider(ReaderMenuTopProvider(viewModel))
viewBinding.toolbarBottom.addMenuProvider( secondaryMenuHost.addMenuProvider(
ReaderMenuBottomProvider(this, readerManager, screenOrientationHelper, this, viewModel), ReaderMenuBottomProvider(this, readerManager, screenOrientationHelper, this, viewModel),
) )
} }
@@ -221,7 +225,7 @@ class ReaderActivity :
} else { } else {
viewBinding.toastView.hide() viewBinding.toastView.hide()
} }
viewBinding.toolbarBottom.invalidateMenu() secondaryMenuHost.invalidateMenu()
invalidateOptionsMenu() invalidateOptionsMenu()
} }
@@ -242,7 +246,7 @@ class ReaderActivity :
rawX >= viewBinding.root.width - gestureInsets.right || rawX >= viewBinding.root.width - gestureInsets.right ||
rawY >= viewBinding.root.height - gestureInsets.bottom || rawY >= viewBinding.root.height - gestureInsets.bottom ||
viewBinding.appbarTop.hasGlobalPoint(rawX, rawY) || viewBinding.appbarTop.hasGlobalPoint(rawX, rawY) ||
viewBinding.appbarBottom.hasGlobalPoint(rawX, rawY) == true viewBinding.appbarBottom?.hasGlobalPoint(rawX, rawY) == true
) { ) {
false false
} else { } else {
@@ -306,7 +310,7 @@ class ReaderActivity :
buttonPrev.isVisible = ReaderControl.PREV_CHAPTER in controls buttonPrev.isVisible = ReaderControl.PREV_CHAPTER in controls
buttonNext.isVisible = ReaderControl.NEXT_CHAPTER in controls buttonNext.isVisible = ReaderControl.NEXT_CHAPTER in controls
slider.isVisible = ReaderControl.SLIDER in controls slider.isVisible = ReaderControl.SLIDER in controls
toolbarBottom.invalidateMenu() secondaryMenuHost.invalidateMenu()
} }
private fun setUiIsVisible(isUiVisible: Boolean) { private fun setUiIsVisible(isUiVisible: Boolean) {
@@ -321,7 +325,7 @@ class ReaderActivity :
} }
val isFullscreen = settings.isReaderFullscreenEnabled val isFullscreen = settings.isReaderFullscreenEnabled
viewBinding.appbarTop.isVisible = isUiVisible viewBinding.appbarTop.isVisible = isUiVisible
viewBinding.appbarBottom.isVisible = isUiVisible viewBinding.appbarBottom?.isVisible = isUiVisible
viewBinding.infoBar.isGone = isUiVisible || (!viewModel.isInfoBarEnabled.value) viewBinding.infoBar.isGone = isUiVisible || (!viewModel.isInfoBarEnabled.value)
viewBinding.infoBar.isTimeVisible = isFullscreen viewBinding.infoBar.isTimeVisible = isFullscreen
systemUiController.setSystemUiVisible(isUiVisible || !isFullscreen) systemUiController.setSystemUiVisible(isUiVisible || !isFullscreen)
@@ -336,7 +340,7 @@ class ReaderActivity :
right = systemBars.right, right = systemBars.right,
left = systemBars.left, left = systemBars.left,
) )
viewBinding.appbarBottom.updateLayoutParams<MarginLayoutParams> { viewBinding.appbarBottom?.updateLayoutParams<MarginLayoutParams> {
bottomMargin = systemBars.bottom + topMargin bottomMargin = systemBars.bottom + topMargin
rightMargin = systemBars.right + topMargin rightMargin = systemBars.right + topMargin
leftMargin = systemBars.left + topMargin leftMargin = systemBars.left + topMargin

View File

@@ -25,6 +25,7 @@ import org.koitharu.kotatsu.databinding.FragmentListBinding
import org.koitharu.kotatsu.filter.ui.FilterCoordinator import org.koitharu.kotatsu.filter.ui.FilterCoordinator
import org.koitharu.kotatsu.list.ui.MangaListFragment import org.koitharu.kotatsu.list.ui.MangaListFragment
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.search.domain.SearchKind
@AndroidEntryPoint @AndroidEntryPoint
class RemoteListFragment : MangaListFragment(), FilterCoordinator.Owner { class RemoteListFragment : MangaListFragment(), FilterCoordinator.Owner {
@@ -72,6 +73,15 @@ class RemoteListFragment : MangaListFragment(), FilterCoordinator.Owner {
} }
} }
override fun onFooterButtonClick() {
val filter = filterCoordinator.snapshot().listFilter
when {
!filter.query.isNullOrEmpty() -> router.openSearch(filter.query.orEmpty(), SearchKind.SIMPLE)
!filter.author.isNullOrEmpty() -> router.openSearch(filter.author.orEmpty(), SearchKind.AUTHOR)
filter.tags.size == 1 -> router.openSearch(filter.tags.singleOrNull()?.title.orEmpty(), SearchKind.TAG)
}
}
override fun onSecondaryErrorActionClick(error: Throwable) { override fun onSecondaryErrorActionClick(error: Throwable) {
openInBrowser(error.getCauseUrl()) openInBrowser(error.getCauseUrl())
} }

View File

@@ -33,6 +33,7 @@ import org.koitharu.kotatsu.explore.domain.ExploreRepository
import org.koitharu.kotatsu.filter.ui.FilterCoordinator import org.koitharu.kotatsu.filter.ui.FilterCoordinator
import org.koitharu.kotatsu.list.domain.MangaListMapper import org.koitharu.kotatsu.list.domain.MangaListMapper
import org.koitharu.kotatsu.list.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.ButtonFooter
import org.koitharu.kotatsu.list.ui.model.EmptyState import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingFooter import org.koitharu.kotatsu.list.ui.model.LoadingFooter
@@ -90,6 +91,7 @@ open class RemoteListViewModel @Inject constructor(
when { when {
error != null -> add(error.toErrorFooter()) error != null -> add(error.toErrorFooter())
hasNext -> add(LoadingFooter()) hasNext -> add(LoadingFooter())
else -> getFooter()?.let(::add)
} }
} }
} }
@@ -178,6 +180,18 @@ open class RemoteListViewModel @Inject constructor(
mode: ListMode mode: ListMode
) = mangaListMapper.toListModelList(destination, manga, mode) ) = mangaListMapper.toListModelList(destination, manga, mode)
protected open fun getFooter(): ButtonFooter? {
val filter = filterCoordinator.snapshot().listFilter
val hasQuery = !filter.query.isNullOrEmpty()
val hasAuthor = !filter.author.isNullOrEmpty()
val isOneTag = filter.tags.size == 1
return if ((hasQuery xor isOneTag xor hasAuthor) && !(hasQuery && isOneTag && hasAuthor)) {
ButtonFooter(R.string.global_search)
} else {
null
}
}
fun openRandom() { fun openRandom() {
if (randomJob?.isActive == true) { if (randomJob?.isActive == true) {
return return

View File

@@ -23,6 +23,7 @@ import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.require import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.core.util.ext.requireValue import org.koitharu.kotatsu.core.util.ext.requireValue
import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.list.domain.ReadingProgress
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingFooter import org.koitharu.kotatsu.list.ui.model.LoadingFooter
import org.koitharu.kotatsu.list.ui.model.LoadingState import org.koitharu.kotatsu.list.ui.model.LoadingState
@@ -159,7 +160,7 @@ class ScrobblingSelectorViewModel @Inject constructor(
rating = prevInfo?.rating ?: 0f, rating = prevInfo?.rating ?: 0f,
status = prevInfo?.status ?: when { status = prevInfo?.status ?: when {
history == null -> ScrobblingStatus.PLANNED history == null -> ScrobblingStatus.PLANNED
history.percent == 1f -> ScrobblingStatus.COMPLETED ReadingProgress.isCompleted(history.percent) -> ScrobblingStatus.COMPLETED
else -> ScrobblingStatus.READING else -> ScrobblingStatus.READING
}, },
comment = prevInfo?.comment, comment = prevInfo?.comment,

View File

@@ -38,8 +38,15 @@ class SearchV2Helper @AssistedInject constructor(
private suspend fun MangaRepository.getFilter(query: String, kind: SearchKind): MangaListFilter? = when (kind) { private suspend fun MangaRepository.getFilter(query: String, kind: SearchKind): MangaListFilter? = when (kind) {
SearchKind.SIMPLE, SearchKind.SIMPLE,
SearchKind.TITLE, SearchKind.TITLE -> if (filterCapabilities.isSearchSupported) {
SearchKind.AUTHOR -> if (filterCapabilities.isSearchSupported) { // TODO author support MangaListFilter(query = query)
} else {
null
}
SearchKind.AUTHOR -> if (filterCapabilities.isAuthorSearchSupported) {
MangaListFilter(author = query)
} else if (filterCapabilities.isSearchSupported) {
MangaListFilter(query = query) MangaListFilter(query = query)
} else { } else {
null null

View File

@@ -15,7 +15,6 @@ data class SearchResultsListModel(
val source: MangaSource, val source: MangaSource,
val listFilter: MangaListFilter?, val listFilter: MangaListFilter?,
val sortOrder: SortOrder?, val sortOrder: SortOrder?,
val hasMore: Boolean,
val list: List<MangaListModel>, val list: List<MangaListModel>,
val error: Throwable?, val error: Throwable?,
) : ListModel { ) : ListModel {

View File

@@ -44,7 +44,6 @@ import org.koitharu.kotatsu.search.domain.SearchV2Helper
import javax.inject.Inject import javax.inject.Inject
private const val MAX_PARALLELISM = 4 private const val MAX_PARALLELISM = 4
private const val MIN_HAS_MORE_ITEMS = 8
@HiltViewModel @HiltViewModel
class SearchViewModel @Inject constructor( class SearchViewModel @Inject constructor(
@@ -132,7 +131,6 @@ class SearchViewModel @Inject constructor(
SearchResultsListModel( SearchResultsListModel(
titleResId = 0, titleResId = 0,
source = source, source = source,
hasMore = list.size > MIN_HAS_MORE_ITEMS,
list = list, list = list,
error = null, error = null,
listFilter = result.listFilter, listFilter = result.listFilter,
@@ -142,7 +140,7 @@ class SearchViewModel @Inject constructor(
}, },
onFailure = { error -> onFailure = { error ->
error.printStackTraceDebug() error.printStackTraceDebug()
SearchResultsListModel(0, source, null, null, true, emptyList(), error) SearchResultsListModel(0, source, null, null, emptyList(), error)
}, },
) )
if (item != null) { if (item != null) {
@@ -163,7 +161,6 @@ class SearchViewModel @Inject constructor(
SearchResultsListModel( SearchResultsListModel(
titleResId = R.string.history, titleResId = R.string.history,
source = UnknownMangaSource, source = UnknownMangaSource,
hasMore = false,
list = mangaListMapper.toListModelList(manga = result, mode = ListMode.GRID), list = mangaListMapper.toListModelList(manga = result, mode = ListMode.GRID),
error = null, error = null,
listFilter = null, listFilter = null,
@@ -177,7 +174,6 @@ class SearchViewModel @Inject constructor(
SearchResultsListModel( SearchResultsListModel(
titleResId = R.string.history, titleResId = R.string.history,
source = UnknownMangaSource, source = UnknownMangaSource,
hasMore = false,
list = emptyList(), list = emptyList(),
error = error, error = error,
listFilter = null, listFilter = null,
@@ -196,7 +192,6 @@ class SearchViewModel @Inject constructor(
SearchResultsListModel( SearchResultsListModel(
titleResId = R.string.favourites, titleResId = R.string.favourites,
source = UnknownMangaSource, source = UnknownMangaSource,
hasMore = false,
list = mangaListMapper.toListModelList( list = mangaListMapper.toListModelList(
manga = result, manga = result,
mode = ListMode.GRID, mode = ListMode.GRID,
@@ -214,7 +209,6 @@ class SearchViewModel @Inject constructor(
SearchResultsListModel( SearchResultsListModel(
titleResId = R.string.favourites, titleResId = R.string.favourites,
source = UnknownMangaSource, source = UnknownMangaSource,
hasMore = false,
list = emptyList(), list = emptyList(),
error = error, error = error,
listFilter = null, listFilter = null,
@@ -233,7 +227,6 @@ class SearchViewModel @Inject constructor(
SearchResultsListModel( SearchResultsListModel(
titleResId = 0, titleResId = 0,
source = LocalMangaSource, source = LocalMangaSource,
hasMore = result.manga.size > MIN_HAS_MORE_ITEMS,
list = mangaListMapper.toListModelList( list = mangaListMapper.toListModelList(
manga = result.manga, manga = result.manga,
mode = ListMode.GRID, mode = ListMode.GRID,
@@ -251,7 +244,6 @@ class SearchViewModel @Inject constructor(
SearchResultsListModel( SearchResultsListModel(
titleResId = 0, titleResId = 0,
source = LocalMangaSource, source = LocalMangaSource,
hasMore = true,
list = emptyList(), list = emptyList(),
error = error, error = error,
listFilter = null, listFilter = null,

View File

@@ -0,0 +1,139 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
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:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:background="@color/grey" />
<org.koitharu.kotatsu.core.ui.widgets.ZoomControl
android:id="@+id/zoomControl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:orientation="vertical"
android:spacing="2dp"
android:visibility="gone"
app:layout_dodgeInsetEdges="bottom"
tools:visibility="visible" />
<org.koitharu.kotatsu.reader.ui.ReaderInfoBarView
android:id="@+id/infoBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:padding="6dp"
android:textSize="12sp"
android:visibility="gone"
tools:visibility="visible" />
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar_top"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="@dimen/m3_card_elevated_elevation"
app:elevation="@dimen/m3_card_elevated_elevation"
app:liftOnScroll="false">
<com.google.android.material.appbar.MaterialToolbar
android:id="@id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="@dimen/m3_card_elevated_elevation"
app:elevation="@dimen/m3_card_elevated_elevation"
app:popupTheme="@style/ThemeOverlay.Kotatsu"
tools:menu="@menu/opt_reader_top">
<LinearLayout
android:id="@+id/layout_slider"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|end"
android:layout_marginEnd="2dp"
android:gravity="center_vertical|end">
<ImageButton
android:id="@+id/button_prev"
style="?actionButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/prev_chapter"
android:src="@drawable/ic_prev"
android:tooltipText="@string/prev_chapter" />
<com.google.android.material.slider.Slider
android:id="@+id/slider"
android:layout_width="260dp"
android:layout_height="wrap_content"
android:stepSize="1.0"
android:valueFrom="0"
app:labelBehavior="floating"
tools:value="6"
tools:valueTo="20" />
<ImageButton
android:id="@+id/button_next"
style="?actionButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/next_chapter"
android:src="@drawable/ic_next"
android:tooltipText="@string/next_chapter" />
</LinearLayout>
</com.google.android.material.appbar.MaterialToolbar>
</com.google.android.material.appbar.AppBarLayout>
<org.koitharu.kotatsu.reader.ui.ReaderToastView
android:id="@+id/toastView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:layout_marginBottom="20dp"
android:background="@drawable/bg_reader_indicator"
android:drawablePadding="6dp"
android:elevation="1000dp"
android:singleLine="true"
android:textAppearance="?attr/textAppearanceBodySmall"
android:theme="@style/ThemeOverlay.Material3.Dark"
app:layout_dodgeInsetEdges="bottom"
tools:text="@string/loading_" />
<LinearLayout
android:id="@+id/layout_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="@drawable/bg_card"
android:backgroundTint="?colorSurfaceContainer"
android:gravity="center_horizontal"
android:orientation="vertical"
android:outlineProvider="background"
android:padding="@dimen/screen_padding">
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true" />
<TextView
android:id="@+id/textView_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@string/loading_"
android:textAppearance="?attr/textAppearanceTitleMedium" />
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -6,7 +6,7 @@
android:id="@+id/scrollView" android:id="@+id/scrollView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:background="@macro/m3_comp_filled_card_container_color"> tools:background="?colorBackgroundFloating">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -122,50 +122,38 @@
app:barrierMargin="8dp" app:barrierMargin="8dp"
app:constraint_referenced_ids="imageView_cover,rating_bar" /> app:constraint_referenced_ids="imageView_cover,rating_bar" />
<org.koitharu.kotatsu.core.ui.widgets.ProgressButton <Button
android:id="@+id/button_read" android:id="@+id/button_read"
style="?materialButtonStyle"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginTop="@dimen/margin_normal" android:layout_marginTop="@dimen/margin_normal"
android:layout_marginEnd="12dp" android:layout_marginEnd="12dp"
android:foreground="?selectableItemBackground"
android:gravity="center"
android:paddingHorizontal="6dp"
android:paddingVertical="8dp"
app:baseColor="@color/m3_chip_background_color"
app:layout_constraintEnd_toStartOf="@id/button_open" app:layout_constraintEnd_toStartOf="@id/button_open"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/barrier_header" app:layout_constraintTop_toBottomOf="@id/barrier_header"
app:progressColor="?colorControlNormal" tools:text="@string/read" />
app:subtitleTextAppearance="?textAppearanceBodySmall"
app:titleTextAppearance="?textAppearanceButton"
tools:max="100"
tools:progress="40"
tools:subtitle="12 chapters"
tools:title="@string/read" />
<ImageView <com.google.android.material.button.MaterialButton
android:id="@+id/button_open" android:id="@+id/button_open"
style="?materialIconButtonStyle"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:background="@drawable/bg_circle_button"
android:backgroundTint="@color/m3_chip_background_color"
android:contentDescription="@string/details" android:contentDescription="@string/details"
android:scaleType="centerInside" app:icon="@drawable/ic_expand"
app:layout_constraintBottom_toBottomOf="@id/button_read" app:layout_constraintBottom_toBottomOf="@id/button_read"
app:layout_constraintDimensionRatio="1" app:layout_constraintDimensionRatio="1"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/button_read" app:layout_constraintTop_toTopOf="@id/button_read" />
app:srcCompat="@drawable/ic_expand" />
<org.koitharu.kotatsu.core.ui.widgets.ChipsView <org.koitharu.kotatsu.core.ui.widgets.ChipsView
android:id="@+id/chips_tags" android:id="@+id/chips_tags"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_normal"
android:layout_marginHorizontal="@dimen/screen_padding" android:layout_marginHorizontal="@dimen/screen_padding"
android:layout_marginTop="@dimen/margin_normal"
app:chipSpacingHorizontal="6dp" app:chipSpacingHorizontal="6dp"
app:chipSpacingVertical="6dp" app:chipSpacingVertical="6dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="4dp">
<Button
android:id="@+id/button"
style="?materialButtonOutlinedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="@string/globally" />
</FrameLayout>

View File

@@ -23,6 +23,19 @@
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover" app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover"
tools:src="@tools:sample/backgrounds/scenic" /> tools:src="@tools:sample/backgrounds/scenic" />
<org.koitharu.kotatsu.core.ui.widgets.IconsView
android:id="@+id/iconsView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:background="@drawable/bg_list_icons"
android:orientation="horizontal"
android:padding="4dp"
app:iconSize="14dp"
app:iconSpacing="4dp"
app:layout_constraintStart_toStartOf="@id/imageView_cover"
app:layout_constraintTop_toTopOf="@id/imageView_cover" />
<org.koitharu.kotatsu.history.ui.util.ReadingProgressView <org.koitharu.kotatsu.history.ui.util.ReadingProgressView
android:id="@+id/progressView" android:id="@+id/progressView"
android:layout_width="@dimen/card_indicator_size" android:layout_width="@dimen/card_indicator_size"

View File

@@ -803,4 +803,5 @@
<string name="badges_in_lists">Badges in lists</string> <string name="badges_in_lists">Badges in lists</string>
<string name="search_everywhere">Search everywhere</string> <string name="search_everywhere">Search everywhere</string>
<string name="simple">Simple</string> <string name="simple">Simple</string>
<string name="global_search">Global search</string>
</resources> </resources>

View File

@@ -31,7 +31,7 @@ material = "1.13.0-alpha10"
moshi = "1.15.2" moshi = "1.15.2"
okhttp = "4.12.0" okhttp = "4.12.0"
okio = "3.10.2" okio = "3.10.2"
parsers = "198e859850" parsers = "88ea5215c0"
preference = "1.2.1" preference = "1.2.1"
recyclerview = "1.4.0" recyclerview = "1.4.0"
room = "2.6.1" room = "2.6.1"