diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/ui/AlternativeAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/ui/AlternativeAD.kt index 74afdba6a..9af518747 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/ui/AlternativeAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/ui/AlternativeAD.kt @@ -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) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/ui/AlternativesViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/ui/AlternativesViewModel.kt index 42ec2ac97..bc76db87f 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/ui/AlternativesViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/ui/AlternativesViewModel.kt @@ -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(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>(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) - } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/ui/MangaAlternativeModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/ui/MangaAlternativeModel.kt index da9d9b5a6..48d3146f8 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/ui/MangaAlternativeModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/ui/MangaAlternativeModel.kt @@ -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 + } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/ProgressButton.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/ProgressButton.kt deleted file mode 100644 index 33f955630..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/ProgressButton.kt +++ /dev/null @@ -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) - } - } -} 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 1925d1ca8..ecb2809af 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 @@ -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()) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt index 0d58c83a0..c94b4d4d8 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt @@ -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( diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderFragment.kt index ca1e30837..c740f1b8b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderFragment.kt @@ -60,7 +60,11 @@ class FilterHeaderFragment : BaseFragment(), 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) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderProducer.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderProducer.kt index e011beaa7..5f8d6d6dd 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderProducer.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderProducer.kt @@ -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()) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt index 060211ddc..80e5e21cb 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt @@ -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, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt index 81de88924..221c58e37 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt @@ -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 }, ) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/ReadingProgress.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/ReadingProgress.kt index 4ce030bf0..ee01f78d8 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/ReadingProgress.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/ReadingProgress.kt @@ -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 } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ButtonFooterAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ButtonFooterAD.kt new file mode 100644 index 000000000..e301b629b --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ButtonFooterAD.kt @@ -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( + { inflater, parent -> ItemButtonFooterBinding.inflate(inflater, parent, false) }, +) { + + binding.button.setOnClickListener { + listener.onFooterButtonClick() + } + + bind { + binding.button.setText(item.textResId) + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ErrorFooterAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ErrorFooterAD.kt index 2bdf5a434..63f533443 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ErrorFooterAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ErrorFooterAD.kt @@ -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( { inflater, parent -> ItemErrorFooterBinding.inflate(inflater, parent, false) }, ) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ListItemType.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ListItemType.kt index 6e2ef517d..5f91fa887 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ListItemType.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ListItemType.kt @@ -15,6 +15,7 @@ enum class ListItemType { MANGA_NESTED_GROUP, FOOTER_LOADING, FOOTER_ERROR, + FOOTER_BUTTON, STATE_LOADING, STATE_ERROR, STATE_EMPTY, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ListStateHolderListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ListStateHolderListener.kt index 0e0d4ad84..44e99f310 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ListStateHolderListener.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ListStateHolderListener.kt @@ -7,4 +7,6 @@ interface ListStateHolderListener { fun onSecondaryErrorActionClick(error: Throwable) = Unit fun onEmptyActionClick() + + fun onFooterButtonClick() = Unit } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt index d683521e4..503299e8c 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt @@ -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)) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/TypedListSpacingDecoration.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/TypedListSpacingDecoration.kt index b56d5a1af..17bb3dcf7 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/TypedListSpacingDecoration.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/TypedListSpacingDecoration.kt @@ -54,6 +54,7 @@ class TypedListSpacingDecoration( ListItemType.FOOTER_LOADING, ListItemType.FOOTER_ERROR, + ListItemType.FOOTER_BUTTON, ListItemType.STATE_LOADING, ListItemType.STATE_ERROR, ListItemType.STATE_EMPTY, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/ButtonFooter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/ButtonFooter.kt new file mode 100644 index 000000000..f39a4e323 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/ButtonFooter.kt @@ -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 + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/preview/PreviewFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/preview/PreviewFragment.kt index 1ba64c264..1e38e9088 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/preview/PreviewFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/preview/PreviewFragment.kt @@ -121,20 +121,14 @@ class PreviewFragment : BaseFragment(), 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 + }, + ) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt index a2c11be7e..b87a2eb17 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt @@ -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 { + viewBinding.appbarBottom?.updateLayoutParams { bottomMargin = systemBars.bottom + topMargin rightMargin = systemBars.right + topMargin leftMargin = systemBars.left + topMargin diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt index 66c70f98c..382e0035b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt @@ -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()) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt index 5c4d8d8dd..958c5fe4d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt @@ -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 diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorViewModel.kt index 37d0f4384..c15974eda 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorViewModel.kt @@ -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, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/domain/SearchV2Helper.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/domain/SearchV2Helper.kt index b6676acb9..a4006ca02 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/search/domain/SearchV2Helper.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/domain/SearchV2Helper.kt @@ -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 diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/SearchResultsListModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/SearchResultsListModel.kt index 2baf21777..1a9bf72ce 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/SearchResultsListModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/SearchResultsListModel.kt @@ -15,7 +15,6 @@ data class SearchResultsListModel( val source: MangaSource, val listFilter: MangaListFilter?, val sortOrder: SortOrder?, - val hasMore: Boolean, val list: List, val error: Throwable?, ) : ListModel { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/SearchViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/SearchViewModel.kt index fa53187d6..ca30a475e 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/SearchViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/SearchViewModel.kt @@ -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, diff --git a/app/src/main/res/layout-w600dp-land/activity_reader.xml b/app/src/main/res/layout-w600dp-land/activity_reader.xml new file mode 100644 index 000000000..cbfbe99be --- /dev/null +++ b/app/src/main/res/layout-w600dp-land/activity_reader.xml @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_preview.xml b/app/src/main/res/layout/fragment_preview.xml index 136505674..2c99d65bd 100644 --- a/app/src/main/res/layout/fragment_preview.xml +++ b/app/src/main/res/layout/fragment_preview.xml @@ -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"> - + tools:text="@string/read" /> - + app:layout_constraintTop_toTopOf="@id/button_read" /> + + +