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.text.buildSpannedString
import androidx.core.text.inSpans
import androidx.core.view.isVisible
import androidx.lifecycle.LifecycleOwner
import coil3.ImageLoader
import coil3.request.ImageRequest
@@ -51,7 +52,13 @@ fun alternativeAD(
binding.chipSource.setOnClickListener(clickListener)
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 {
if (item.chaptersCount > 0) {
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 ->
chip.text = item.manga.source.getTitle(chip.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.nav.AppRouter
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.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.list.domain.ReadingProgress
import org.koitharu.kotatsu.list.domain.MangaListMapper
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
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.util.runCatchingCancellable
import javax.inject.Inject
@@ -36,8 +36,7 @@ class AlternativesViewModel @Inject constructor(
private val mangaRepositoryFactory: MangaRepository.Factory,
private val alternativesUseCase: AlternativesUseCase,
private val migrateUseCase: MigrateUseCase,
private val historyRepository: HistoryRepository,
private val settings: AppSettings,
private val mangaListMapper: MangaListMapper,
) : BaseViewModel() {
val manga = savedStateHandle.require<ParcelableManga>(AppRouter.KEY_MANGA).manga
@@ -55,8 +54,7 @@ class AlternativesViewModel @Inject constructor(
alternativesUseCase(ref)
.map {
MangaAlternativeModel(
manga = it,
progress = getProgress(it.id),
mangaModel = mangaListMapper.toListModel(it, ListMode.GRID) as MangaGridModel,
referenceChapters = refCount,
)
}.runningFold<MangaAlternativeModel, List<ListModel>>(listOf(LoadingState)) { acc, item ->
@@ -88,8 +86,4 @@ class AlternativesViewModel @Inject constructor(
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
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.MangaGridModel
import org.koitharu.kotatsu.parsers.model.Manga
data class MangaAlternativeModel(
val manga: Manga,
val progress: ReadingProgress?,
val mangaModel: MangaGridModel,
private val referenceChapters: Int,
) : ListModel {
val manga: Manga
get() = mangaModel.manga
val chaptersCount = manga.chaptersCount()
val chaptersDiff: Int
@@ -19,4 +21,10 @@ data class MangaAlternativeModel(
override fun areItemsTheSame(other: ListModel): Boolean {
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.download.ui.worker.DownloadStartedObserver
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.mangaGridItemAD
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.util.ifNullOrEmpty
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
import org.koitharu.kotatsu.search.domain.SearchKind
import javax.inject.Inject
import kotlin.math.roundToInt
import com.google.android.material.R as materialR
@@ -203,8 +205,8 @@ class DetailsActivity :
override fun onClick(v: View) {
when (v.id) {
R.id.textView_author -> {
val manga = viewModel.manga.value ?: return
router.openSearch(manga.source, manga.author ?: return)
val author = viewModel.manga.value?.author ?: return
router.openSearch(author, SearchKind.AUTHOR)
}
R.id.textView_source -> {
@@ -484,7 +486,7 @@ class DetailsActivity :
textViewProgress.textAndVisible = if (info.percent <= 0f) {
null
} 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())
}

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?) {
currentListFilter.update { oldValue ->
oldValue.copy(

View File

@@ -60,7 +60,11 @@ class FilterHeaderFragment : BaseFragment<FragmentFilterHeaderBinding>(), ChipsV
override fun onChipCloseClick(chip: Chip, data: Any?) {
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 Demographic -> filter.toggleDemographic(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 }
if (hasTags) {
result.addLast(moreTagsChip())

View File

@@ -154,7 +154,7 @@ class HistoryRepository @Inject constructor(
suspend fun getProgress(mangaId: Long, mode: ProgressIndicatorMode): ReadingProgress? {
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(
percent = fixedPercent,
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.MangaListMapper
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.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.InfoModel
@@ -207,10 +208,10 @@ class HistoryListViewModel @Inject constructor(
ListSortOrder.UNREAD,
ListSortOrder.PROGRESS -> ListHeader(
when (percent) {
1f -> R.string.status_completed
in 0f..0.01f -> R.string.status_planned
in 0f..1f -> R.string.status_reading
when {
ReadingProgress.isCompleted(percent) -> R.string.status_completed
percent in 0f..0.01f -> R.string.status_planned
percent in 0f..1f -> R.string.status_reading
else -> R.string.unknown
},
)

View File

@@ -39,9 +39,10 @@ data class ReadingProgress(
const val PROGRESS_NONE = -1f
const val PROGRESS_COMPLETED = 1f
private const val PROGRESS_COMPLETED_THRESHOLD = 0.99999f
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
fun errorFooterAD(
listener: MangaListListener?,
listener: ListStateHolderListener?,
) = adapterDelegateViewBinding<ErrorFooter, ListModel, ItemErrorFooterBinding>(
{ inflater, parent -> ItemErrorFooterBinding.inflate(inflater, parent, false) },
) {

View File

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

View File

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

View File

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

View File

@@ -54,6 +54,7 @@ class TypedListSpacingDecoration(
ListItemType.FOOTER_LOADING,
ListItemType.FOOTER_ERROR,
ListItemType.FOOTER_BUTTON,
ListItemType.STATE_LOADING,
ListItemType.STATE_ERROR,
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?) {
with(requireViewBinding()) {
buttonRead.isEnabled = footer != null
buttonRead.setTitle(if (footer?.isInProgress() == true) R.string._continue else R.string.read)
buttonRead.subtitle = when {
footer == null -> getString(R.string.loading_)
footer.isIncognito -> getString(R.string.incognito_mode)
footer.currentChapter >= 0 -> getString(
R.string.chapter_d_of_d,
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)
buttonRead.setText(
when {
footer == null -> R.string.loading_
footer.isIncognito == true -> R.string.incognito
footer.isInProgress() == true -> R.string._continue
else -> R.string.read
},
)
}
}

View File

@@ -14,6 +14,7 @@ import android.view.ViewGroup.MarginLayoutParams
import android.view.WindowManager
import androidx.activity.viewModels
import androidx.core.graphics.Insets
import androidx.core.view.MenuHost
import androidx.core.view.OnApplyWindowInsetsListener
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isGone
@@ -98,6 +99,9 @@ class ReaderActivity :
scrollTimer.isEnabled = value
}
private val secondaryMenuHost: MenuHost
get() = viewBinding.toolbarBottom ?: this
private lateinit var scrollTimer: ScrollTimer
private lateinit var pageSaveHelper: PageSaveHelper
private lateinit var touchHelper: TapGridDispatcher
@@ -150,7 +154,7 @@ class ReaderActivity :
viewModel.isInfoBarTransparent.observe(this) { viewBinding.infoBar.drawBackground = !it }
viewModel.isInfoBarEnabled.observe(this, ::onReaderBarChanged)
viewModel.isBookmarkAdded.observe(this, MenuInvalidator(this))
val bottomMenuInvalidator = MenuInvalidator(viewBinding.toolbarBottom)
val bottomMenuInvalidator = MenuInvalidator(secondaryMenuHost)
viewModel.isPagesSheetEnabled.observe(this, bottomMenuInvalidator)
screenOrientationHelper.observeAutoOrientation().observe(this, bottomMenuInvalidator)
viewModel.onShowToast.observeEvent(this) { msgId ->
@@ -165,7 +169,7 @@ class ReaderActivity :
viewBinding.zoomControl.isVisible = it
}
addMenuProvider(ReaderMenuTopProvider(viewModel))
viewBinding.toolbarBottom.addMenuProvider(
secondaryMenuHost.addMenuProvider(
ReaderMenuBottomProvider(this, readerManager, screenOrientationHelper, this, viewModel),
)
}
@@ -221,7 +225,7 @@ class ReaderActivity :
} else {
viewBinding.toastView.hide()
}
viewBinding.toolbarBottom.invalidateMenu()
secondaryMenuHost.invalidateMenu()
invalidateOptionsMenu()
}
@@ -242,7 +246,7 @@ class ReaderActivity :
rawX >= viewBinding.root.width - gestureInsets.right ||
rawY >= viewBinding.root.height - gestureInsets.bottom ||
viewBinding.appbarTop.hasGlobalPoint(rawX, rawY) ||
viewBinding.appbarBottom.hasGlobalPoint(rawX, rawY) == true
viewBinding.appbarBottom?.hasGlobalPoint(rawX, rawY) == true
) {
false
} else {
@@ -306,7 +310,7 @@ class ReaderActivity :
buttonPrev.isVisible = ReaderControl.PREV_CHAPTER in controls
buttonNext.isVisible = ReaderControl.NEXT_CHAPTER in controls
slider.isVisible = ReaderControl.SLIDER in controls
toolbarBottom.invalidateMenu()
secondaryMenuHost.invalidateMenu()
}
private fun setUiIsVisible(isUiVisible: Boolean) {
@@ -321,7 +325,7 @@ class ReaderActivity :
}
val isFullscreen = settings.isReaderFullscreenEnabled
viewBinding.appbarTop.isVisible = isUiVisible
viewBinding.appbarBottom.isVisible = isUiVisible
viewBinding.appbarBottom?.isVisible = isUiVisible
viewBinding.infoBar.isGone = isUiVisible || (!viewModel.isInfoBarEnabled.value)
viewBinding.infoBar.isTimeVisible = isFullscreen
systemUiController.setSystemUiVisible(isUiVisible || !isFullscreen)
@@ -336,7 +340,7 @@ class ReaderActivity :
right = systemBars.right,
left = systemBars.left,
)
viewBinding.appbarBottom.updateLayoutParams<MarginLayoutParams> {
viewBinding.appbarBottom?.updateLayoutParams<MarginLayoutParams> {
bottomMargin = systemBars.bottom + topMargin
rightMargin = systemBars.right + 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.list.ui.MangaListFragment
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.search.domain.SearchKind
@AndroidEntryPoint
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) {
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.list.domain.MangaListMapper
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.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
@@ -90,6 +91,7 @@ open class RemoteListViewModel @Inject constructor(
when {
error != null -> add(error.toErrorFooter())
hasNext -> add(LoadingFooter())
else -> getFooter()?.let(::add)
}
}
}
@@ -178,6 +180,18 @@ open class RemoteListViewModel @Inject constructor(
mode: ListMode
) = 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() {
if (randomJob?.isActive == true) {
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.requireValue
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.LoadingFooter
import org.koitharu.kotatsu.list.ui.model.LoadingState
@@ -159,7 +160,7 @@ class ScrobblingSelectorViewModel @Inject constructor(
rating = prevInfo?.rating ?: 0f,
status = prevInfo?.status ?: when {
history == null -> ScrobblingStatus.PLANNED
history.percent == 1f -> ScrobblingStatus.COMPLETED
ReadingProgress.isCompleted(history.percent) -> ScrobblingStatus.COMPLETED
else -> ScrobblingStatus.READING
},
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) {
SearchKind.SIMPLE,
SearchKind.TITLE,
SearchKind.AUTHOR -> if (filterCapabilities.isSearchSupported) { // TODO author support
SearchKind.TITLE -> if (filterCapabilities.isSearchSupported) {
MangaListFilter(query = query)
} else {
null
}
SearchKind.AUTHOR -> if (filterCapabilities.isAuthorSearchSupported) {
MangaListFilter(author = query)
} else if (filterCapabilities.isSearchSupported) {
MangaListFilter(query = query)
} else {
null

View File

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

View File

@@ -44,7 +44,6 @@ import org.koitharu.kotatsu.search.domain.SearchV2Helper
import javax.inject.Inject
private const val MAX_PARALLELISM = 4
private const val MIN_HAS_MORE_ITEMS = 8
@HiltViewModel
class SearchViewModel @Inject constructor(
@@ -132,7 +131,6 @@ class SearchViewModel @Inject constructor(
SearchResultsListModel(
titleResId = 0,
source = source,
hasMore = list.size > MIN_HAS_MORE_ITEMS,
list = list,
error = null,
listFilter = result.listFilter,
@@ -142,7 +140,7 @@ class SearchViewModel @Inject constructor(
},
onFailure = { error ->
error.printStackTraceDebug()
SearchResultsListModel(0, source, null, null, true, emptyList(), error)
SearchResultsListModel(0, source, null, null, emptyList(), error)
},
)
if (item != null) {
@@ -163,7 +161,6 @@ class SearchViewModel @Inject constructor(
SearchResultsListModel(
titleResId = R.string.history,
source = UnknownMangaSource,
hasMore = false,
list = mangaListMapper.toListModelList(manga = result, mode = ListMode.GRID),
error = null,
listFilter = null,
@@ -177,7 +174,6 @@ class SearchViewModel @Inject constructor(
SearchResultsListModel(
titleResId = R.string.history,
source = UnknownMangaSource,
hasMore = false,
list = emptyList(),
error = error,
listFilter = null,
@@ -196,7 +192,6 @@ class SearchViewModel @Inject constructor(
SearchResultsListModel(
titleResId = R.string.favourites,
source = UnknownMangaSource,
hasMore = false,
list = mangaListMapper.toListModelList(
manga = result,
mode = ListMode.GRID,
@@ -214,7 +209,6 @@ class SearchViewModel @Inject constructor(
SearchResultsListModel(
titleResId = R.string.favourites,
source = UnknownMangaSource,
hasMore = false,
list = emptyList(),
error = error,
listFilter = null,
@@ -233,7 +227,6 @@ class SearchViewModel @Inject constructor(
SearchResultsListModel(
titleResId = 0,
source = LocalMangaSource,
hasMore = result.manga.size > MIN_HAS_MORE_ITEMS,
list = mangaListMapper.toListModelList(
manga = result.manga,
mode = ListMode.GRID,
@@ -251,7 +244,6 @@ class SearchViewModel @Inject constructor(
SearchResultsListModel(
titleResId = 0,
source = LocalMangaSource,
hasMore = true,
list = emptyList(),
error = error,
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:layout_width="match_parent"
android:layout_height="match_parent"
tools:background="@macro/m3_comp_filled_card_container_color">
tools:background="?colorBackgroundFloating">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
@@ -122,50 +122,38 @@
app:barrierMargin="8dp"
app:constraint_referenced_ids="imageView_cover,rating_bar" />
<org.koitharu.kotatsu.core.ui.widgets.ProgressButton
<Button
android:id="@+id/button_read"
style="?materialButtonStyle"
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="@color/m3_chip_background_color"
app:layout_constraintEnd_toStartOf="@id/button_open"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/barrier_header"
app:progressColor="?colorControlNormal"
app:subtitleTextAppearance="?textAppearanceBodySmall"
app:titleTextAppearance="?textAppearanceButton"
tools:max="100"
tools:progress="40"
tools:subtitle="12 chapters"
tools:title="@string/read" />
tools:text="@string/read" />
<ImageView
<com.google.android.material.button.MaterialButton
android:id="@+id/button_open"
style="?materialIconButtonStyle"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginEnd="16dp"
android:background="@drawable/bg_circle_button"
android:backgroundTint="@color/m3_chip_background_color"
android:contentDescription="@string/details"
android:scaleType="centerInside"
app:icon="@drawable/ic_expand"
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_expand" />
app:layout_constraintTop_toTopOf="@id/button_read" />
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
android:id="@+id/chips_tags"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_normal"
android:layout_marginHorizontal="@dimen/screen_padding"
android:layout_marginTop="@dimen/margin_normal"
app:chipSpacingHorizontal="6dp"
app:chipSpacingVertical="6dp"
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"
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
android:id="@+id/progressView"
android:layout_width="@dimen/card_indicator_size"

View File

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

View File

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