Update reader ui

This commit is contained in:
Koitharu
2024-12-23 09:48:50 +02:00
parent 22d203fc60
commit 099362d198
22 changed files with 234 additions and 361 deletions

View File

@@ -363,10 +363,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val isReaderBarEnabled: Boolean val isReaderBarEnabled: Boolean
get() = prefs.getBoolean(KEY_READER_BAR, true) get() = prefs.getBoolean(KEY_READER_BAR, true)
var isReaderSliderEnabled: Boolean
get() = prefs.getBoolean(KEY_READER_SLIDER, true)
set(value) = prefs.edit { putBoolean(KEY_READER_SLIDER, value) }
val isReaderKeepScreenOn: Boolean val isReaderKeepScreenOn: Boolean
get() = prefs.getBoolean(KEY_READER_SCREEN_ON, true) get() = prefs.getBoolean(KEY_READER_SCREEN_ON, true)
@@ -671,7 +667,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_SYNC = "sync" const val KEY_SYNC = "sync"
const val KEY_SYNC_SETTINGS = "sync_settings" const val KEY_SYNC_SETTINGS = "sync_settings"
const val KEY_READER_BAR = "reader_bar" const val KEY_READER_BAR = "reader_bar"
const val KEY_READER_SLIDER = "reader_slider"
const val KEY_READER_BACKGROUND = "reader_background" const val KEY_READER_BACKGROUND = "reader_background"
const val KEY_READER_SCREEN_ON = "reader_screen_on" const val KEY_READER_SCREEN_ON = "reader_screen_on"
const val KEY_SHORTCUTS = "dynamic_shortcuts" const val KEY_SHORTCUTS = "dynamic_shortcuts"

View File

@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.core.ui.widgets
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
import android.view.View.OnClickListener
import androidx.annotation.ColorRes import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
@@ -34,7 +33,7 @@ import com.google.android.material.R as materialR
class ChipsView @JvmOverloads constructor( class ChipsView @JvmOverloads constructor(
context: Context, context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = com.google.android.material.R.attr.chipGroupStyle, defStyleAttr: Int = materialR.attr.chipGroupStyle,
) : ChipGroup(context, attrs, defStyleAttr) { ) : ChipGroup(context, attrs, defStyleAttr) {
@Inject @Inject
@@ -49,6 +48,7 @@ class ChipsView @JvmOverloads constructor(
onChipCloseClickListener?.onChipCloseClick(chip, data) ?: onChipClickListener?.onChipClick(chip, data) onChipCloseClickListener?.onChipCloseClick(chip, data) ?: onChipClickListener?.onChipClick(chip, data)
} }
private val chipStyle: Int private val chipStyle: Int
private val iconsVisible: Boolean
var onChipClickListener: OnChipClickListener? = null var onChipClickListener: OnChipClickListener? = null
set(value) { set(value) {
field = value field = value
@@ -60,6 +60,7 @@ class ChipsView @JvmOverloads constructor(
init { init {
val ta = context.obtainStyledAttributes(attrs, R.styleable.ChipsView, defStyleAttr, 0) val ta = context.obtainStyledAttributes(attrs, R.styleable.ChipsView, defStyleAttr, 0)
chipStyle = ta.getResourceId(R.styleable.ChipsView_chipStyle, R.style.Widget_Kotatsu_Chip) chipStyle = ta.getResourceId(R.styleable.ChipsView_chipStyle, R.style.Widget_Kotatsu_Chip)
iconsVisible = ta.getBoolean(R.styleable.ChipsView_chipIconVisible, true)
ta.recycle() ta.recycle()
if (isInEditMode) { if (isInEditMode) {
@@ -170,12 +171,7 @@ class ChipsView @JvmOverloads constructor(
private fun bindIcon(model: ChipModel) { private fun bindIcon(model: ChipModel) {
when { when {
model.isChecked -> { model.isChecked -> disposeIcon()
imageRequest?.dispose()
imageRequest = null
chipIcon = null
isChipIconVisible = false
}
model.isLoading -> { model.isLoading -> {
imageRequest?.dispose() imageRequest?.dispose()
@@ -184,6 +180,8 @@ class ChipsView @JvmOverloads constructor(
setProgressIcon() setProgressIcon()
} }
!iconsVisible -> disposeIcon()
model.iconData != null -> { model.iconData != null -> {
val placeholder = model.icon.ifZero { materialR.drawable.navigation_empty_icon } val placeholder = model.icon.ifZero { materialR.drawable.navigation_empty_icon }
imageRequest = ImageRequest.Builder(context) imageRequest = ImageRequest.Builder(context)
@@ -207,14 +205,16 @@ class ChipsView @JvmOverloads constructor(
isChipIconVisible = true isChipIconVisible = true
} }
else -> { else -> disposeIcon()
imageRequest?.dispose()
imageRequest = null
chipIcon = null
isChipIconVisible = false
}
} }
} }
private fun disposeIcon() {
imageRequest?.dispose()
imageRequest = null
chipIcon = null
isChipIconVisible = false
}
} }
private inner class InternalChipClickListener : OnClickListener { private inner class InternalChipClickListener : OnClickListener {

View File

@@ -4,14 +4,14 @@ import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.LinearLayout import com.google.android.material.button.MaterialButtonGroup
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.databinding.ViewZoomBinding import org.koitharu.kotatsu.databinding.ViewZoomBinding
class ZoomControl @JvmOverloads constructor( class ZoomControl @JvmOverloads constructor(
context: Context, context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
) : LinearLayout(context, attrs), View.OnClickListener { ) : MaterialButtonGroup(context, attrs), View.OnClickListener {
private val binding = ViewZoomBinding.inflate(LayoutInflater.from(context), this) private val binding = ViewZoomBinding.inflate(LayoutInflater.from(context), this)

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.core.util package org.koitharu.kotatsu.core.util
import androidx.core.os.LocaleListCompat import androidx.core.os.LocaleListCompat
import org.koitharu.kotatsu.core.util.ext.indexOfContains
import org.koitharu.kotatsu.core.util.ext.iterator import org.koitharu.kotatsu.core.util.ext.iterator
class LocaleStringComparator : Comparator<String?> { class LocaleStringComparator : Comparator<String?> {
@@ -14,7 +15,7 @@ class LocaleStringComparator : Comparator<String?> {
val set = HashSet<String?>(localeList.size() + 1) val set = HashSet<String?>(localeList.size() + 1)
set.add(null) set.add(null)
for (locale in localeList) { for (locale in localeList) {
val lang = locale.getDisplayLanguage(locale).lowercase() val lang = locale.getDisplayLanguage(locale)
if (set.add(lang)) { if (set.add(lang)) {
add(lang) add(lang)
} }
@@ -23,8 +24,8 @@ class LocaleStringComparator : Comparator<String?> {
} }
override fun compare(a: String?, b: String?): Int { override fun compare(a: String?, b: String?): Int {
val indexA = deviceLocales.indexOf(a?.lowercase()) val indexA = deviceLocales.indexOfContains(a, true)
val indexB = deviceLocales.indexOf(b?.lowercase()) val indexB = deviceLocales.indexOfContains(b, true)
return when { return when {
indexA < 0 && indexB < 0 -> compareValues(a, b) indexA < 0 && indexB < 0 -> compareValues(a, b)
indexA < 0 -> 1 indexA < 0 -> 1

View File

@@ -108,3 +108,7 @@ fun <T, R> Collection<T>.mapSortedByCount(isDescending: Boolean = true, mapper:
} }
return sorted.map { it.first } return sorted.map { it.first }
} }
fun Collection<CharSequence?>.indexOfContains(element: CharSequence?, ignoreCase: Boolean): Int = indexOfFirst { x ->
(x == null && element == null) || (x != null && element != null && x.contains(element, ignoreCase))
}

View File

@@ -91,8 +91,8 @@ class ChaptersPagesSheet : BaseAdaptiveSheet<SheetChaptersPagesBinding>(), Actio
} }
val binding = viewBinding ?: return val binding = viewBinding ?: return
val isActionModeStarted = actionModeDelegate?.isActionModeStarted == true val isActionModeStarted = actionModeDelegate?.isActionModeStarted == true
binding.toolbar.menuView?.isVisible = newState != STATE_COLLAPSED && !isActionModeStarted binding.toolbar.menuView?.isVisible = newState == STATE_EXPANDED && !isActionModeStarted
binding.splitButtonRead.isVisible = newState == STATE_COLLAPSED && !isActionModeStarted binding.splitButtonRead.isVisible = newState != STATE_EXPANDED && !isActionModeStarted
&& viewModel is DetailsViewModel && viewModel is DetailsViewModel
} }

View File

@@ -145,12 +145,10 @@ class ReaderActivity :
viewModel.content.observe(this) { viewModel.content.observe(this) {
onLoadingStateChanged(viewModel.isLoading.value) onLoadingStateChanged(viewModel.isLoading.value)
} }
viewModel.isSliderVisible.observe(this) { updateSliderVisibility() }
viewModel.isKeepScreenOnEnabled.observe(this, this::setKeepScreenOn) viewModel.isKeepScreenOnEnabled.observe(this, this::setKeepScreenOn)
viewModel.isInfoBarEnabled.observe(this, ::onReaderBarChanged) viewModel.isInfoBarEnabled.observe(this, ::onReaderBarChanged)
val bottomMenuInvalidator = MenuInvalidator(viewBinding.toolbarBottom) viewModel.isBookmarkAdded.observe(this, MenuInvalidator(this))
viewModel.isBookmarkAdded.observe(this, bottomMenuInvalidator) viewModel.isPagesSheetEnabled.observe(this, MenuInvalidator(viewBinding.toolbarBottom))
viewModel.isPagesSheetEnabled.observe(this, bottomMenuInvalidator)
viewModel.onShowToast.observeEvent(this) { msgId -> viewModel.onShowToast.observeEvent(this) { msgId ->
Snackbar.make(viewBinding.container, msgId, Snackbar.LENGTH_SHORT) Snackbar.make(viewBinding.container, msgId, Snackbar.LENGTH_SHORT)
.setAnchorView(viewBinding.appbarBottom) .setAnchorView(viewBinding.appbarBottom)
@@ -159,7 +157,8 @@ class ReaderActivity :
viewModel.isZoomControlsEnabled.observe(this) { viewModel.isZoomControlsEnabled.observe(this) {
viewBinding.zoomControl.isVisible = it viewBinding.zoomControl.isVisible = it
} }
viewBinding.toolbarBottom.addMenuProvider(ReaderMenuProvider(this, readerManager, viewModel)) addMenuProvider(ReaderMenuTopProvider(viewModel))
viewBinding.toolbarBottom.addMenuProvider(ReaderMenuBottomProvider(this, readerManager, viewModel))
} }
override fun getParentActivityIntent(): Intent? { override fun getParentActivityIntent(): Intent? {
@@ -234,7 +233,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 {
@@ -301,20 +300,14 @@ class ReaderActivity :
.setOrdering(TransitionSet.ORDERING_TOGETHER) .setOrdering(TransitionSet.ORDERING_TOGETHER)
.addTransition(Slide(Gravity.TOP).addTarget(viewBinding.appbarTop)) .addTransition(Slide(Gravity.TOP).addTarget(viewBinding.appbarTop))
.addTransition(Fade().addTarget(viewBinding.infoBar)) .addTransition(Fade().addTarget(viewBinding.infoBar))
viewBinding.appbarBottom?.let { bottomBar -> .addTransition(Slide(Gravity.BOTTOM).addTarget(viewBinding.appbarBottom))
transition.addTransition(Slide(Gravity.BOTTOM).addTarget(bottomBar))
transition.addTransition(Slide(Gravity.BOTTOM).addTarget(viewBinding.floatingToolbar))
} ?: run {
transition.addTransition(Slide(Gravity.END).addTarget(viewBinding.floatingToolbar))
}
TransitionManager.beginDelayedTransition(viewBinding.root, transition) TransitionManager.beginDelayedTransition(viewBinding.root, transition)
} }
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
updateSliderVisibility()
systemUiController.setSystemUiVisible(isUiVisible || !isFullscreen) systemUiController.setSystemUiVisible(isUiVisible || !isFullscreen)
} }
} }
@@ -327,7 +320,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
@@ -383,33 +376,31 @@ class ReaderActivity :
viewBinding.infoBar.update(uiState) viewBinding.infoBar.update(uiState)
if (uiState == null) { if (uiState == null) {
supportActionBar?.subtitle = null supportActionBar?.subtitle = null
updateSliderVisibility() viewBinding.layoutSlider.isVisible = false
return return
} }
supportActionBar?.subtitle = when { supportActionBar?.subtitle = when {
uiState.incognito -> getString(R.string.incognito_mode) uiState.incognito -> getString(R.string.incognito_mode)
else -> uiState.chapterName else -> uiState.chapterName
} }
if (previous?.chapterName != null && uiState.chapterName != previous.chapterName) { if (uiState.chapterName != previous?.chapterName && !uiState.chapterName.isNullOrEmpty()) {
if (!uiState.chapterName.isNullOrEmpty()) { viewBinding.toastView.showTemporary(uiState.chapterName, TOAST_DURATION)
viewBinding.toastView.showTemporary(uiState.chapterName, TOAST_DURATION)
}
} }
if (uiState.isSliderAvailable()) { if (uiState.isSliderAvailable()) {
viewBinding.slider.valueTo = uiState.totalPages.toFloat() - 1 viewBinding.slider.valueTo = uiState.totalPages.toFloat() - 1
viewBinding.slider.setValueRounded(uiState.currentPage.toFloat()) viewBinding.slider.setValueRounded(uiState.currentPage.toFloat())
} else {
viewBinding.slider.valueTo = 1f
viewBinding.slider.value = 0f
} }
updateSliderVisibility() viewBinding.slider.isEnabled = uiState.isSliderAvailable()
} viewBinding.buttonNext.isEnabled = uiState.hasNextChapter()
viewBinding.buttonPrev.isEnabled = uiState.hasPreviousChapter()
private fun updateSliderVisibility() { viewBinding.layoutSlider.isVisible = true
viewBinding.floatingToolbar.isVisible = viewBinding.appbarTop.isVisible &&
viewModel.isSliderVisible.value &&
viewModel.uiState.value?.isSliderAvailable() == true
} }
companion object { companion object {
private const val TOAST_DURATION = 1500L private const val TOAST_DURATION = 2000L
} }
} }

View File

@@ -8,14 +8,14 @@ import androidx.fragment.app.FragmentActivity
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.nav.router import org.koitharu.kotatsu.core.nav.router
class ReaderMenuProvider( class ReaderMenuBottomProvider(
private val activity: FragmentActivity, private val activity: FragmentActivity,
private val readerManager: ReaderManager, private val readerManager: ReaderManager,
private val viewModel: ReaderViewModel, private val viewModel: ReaderViewModel,
) : MenuProvider { ) : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.opt_reader, menu) menuInflater.inflate(R.menu.opt_reader_bottom, menu)
onPrepareMenu(menu) // fix, not called in toolbar onPrepareMenu(menu) // fix, not called in toolbar
} }
@@ -27,15 +27,6 @@ class ReaderMenuProvider(
setIcon(if (viewModel.isPagesSheetEnabled.value) R.drawable.ic_grid else R.drawable.ic_list) setIcon(if (viewModel.isPagesSheetEnabled.value) R.drawable.ic_grid else R.drawable.ic_list)
} }
} }
menu.findItem(R.id.action_bookmark)?.let { bookmarkItem ->
val hasPages = viewModel.content.value.pages.isNotEmpty()
bookmarkItem.isEnabled = hasPages
if (hasPages) {
val hasBookmark = viewModel.isBookmarkAdded.value
bookmarkItem.setTitle(if (hasBookmark) R.string.bookmark_remove else R.string.bookmark_add)
bookmarkItem.setIcon(if (hasBookmark) R.drawable.ic_bookmark_added else R.drawable.ic_bookmark)
}
}
} }
override fun onMenuItemSelected(menuItem: MenuItem): Boolean { override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
@@ -52,11 +43,6 @@ class ReaderMenuProvider(
true true
} }
R.id.action_slider -> {
viewModel.setSliderVisibility(!viewModel.isSliderVisible.value)
true
}
R.id.action_bookmark -> { R.id.action_bookmark -> {
if (viewModel.isBookmarkAdded.value) { if (viewModel.isBookmarkAdded.value) {
viewModel.removeBookmark() viewModel.removeBookmark()

View File

@@ -0,0 +1,43 @@
package org.koitharu.kotatsu.reader.ui
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.core.view.MenuProvider
import org.koitharu.kotatsu.R
class ReaderMenuTopProvider(
private val viewModel: ReaderViewModel,
) : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.opt_reader_top, menu)
}
override fun onPrepareMenu(menu: Menu) {
menu.findItem(R.id.action_bookmark)?.let { bookmarkItem ->
val hasPages = viewModel.content.value.pages.isNotEmpty()
bookmarkItem.isEnabled = hasPages
if (hasPages) {
val hasBookmark = viewModel.isBookmarkAdded.value
bookmarkItem.setTitle(if (hasBookmark) R.string.bookmark_remove else R.string.bookmark_add)
bookmarkItem.setIcon(if (hasBookmark) R.drawable.ic_bookmark_added else R.drawable.ic_bookmark)
}
}
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
R.id.action_bookmark -> {
if (viewModel.isBookmarkAdded.value) {
viewModel.removeBookmark()
} else {
viewModel.addBookmark()
}
true
}
else -> false
}
}
}

View File

@@ -1,16 +1,19 @@
package org.koitharu.kotatsu.reader.ui package org.koitharu.kotatsu.reader.ui
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.Gravity import android.view.ViewPropertyAnimator
import android.view.ViewGroup import android.view.animation.AccelerateInterpolator
import android.view.animation.DecelerateInterpolator
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.transition.Fade
import androidx.transition.Slide
import androidx.transition.TransitionManager
import androidx.transition.TransitionSet
import com.google.android.material.textview.MaterialTextView import com.google.android.material.textview.MaterialTextView
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
class ReaderToastView @JvmOverloads constructor( class ReaderToastView @JvmOverloads constructor(
context: Context, context: Context,
@@ -18,6 +21,8 @@ class ReaderToastView @JvmOverloads constructor(
defStyleAttr: Int = 0, defStyleAttr: Int = 0,
) : MaterialTextView(context, attrs, defStyleAttr) { ) : MaterialTextView(context, attrs, defStyleAttr) {
private var currentAnimator: ViewPropertyAnimator? = null
private var hideRunnable = Runnable { private var hideRunnable = Runnable {
hide() hide()
} }
@@ -25,8 +30,7 @@ class ReaderToastView @JvmOverloads constructor(
fun show(message: CharSequence) { fun show(message: CharSequence) {
removeCallbacks(hideRunnable) removeCallbacks(hideRunnable)
text = message text = message
setupTransition() showImpl()
isVisible = true
} }
fun show(@StringRes messageId: Int) { fun show(@StringRes messageId: Int) {
@@ -40,8 +44,7 @@ class ReaderToastView @JvmOverloads constructor(
fun hide() { fun hide() {
removeCallbacks(hideRunnable) removeCallbacks(hideRunnable)
setupTransition() hideImpl()
isVisible = false
} }
override fun onDetachedFromWindow() { override fun onDetachedFromWindow() {
@@ -49,13 +52,41 @@ class ReaderToastView @JvmOverloads constructor(
super.onDetachedFromWindow() super.onDetachedFromWindow()
} }
private fun setupTransition() { private fun showImpl() {
val parentView = parent as? ViewGroup ?: return currentAnimator?.cancel()
val transition = TransitionSet() clearAnimation()
.setOrdering(TransitionSet.ORDERING_TOGETHER) if (!context.isAnimationsEnabled) {
.addTarget(this) currentAnimator = null
.addTransition(Slide(Gravity.BOTTOM)) isVisible = true
.addTransition(Fade()) return
TransitionManager.beginDelayedTransition(parentView, transition) }
alpha = 0f
isVisible = true
currentAnimator = animate()
.alpha(1f)
.setInterpolator(DecelerateInterpolator())
.setDuration(context.getAnimationDuration(R.integer.config_shorterAnimTime))
.setListener(null)
} }
}
private fun hideImpl() {
currentAnimator?.cancel()
clearAnimation()
if (!context.isAnimationsEnabled) {
currentAnimator = null
isGone = true
return
}
currentAnimator = animate()
.alpha(0f)
.setInterpolator(AccelerateInterpolator())
.setDuration(context.getAnimationDuration(R.integer.config_shorterAnimTime))
.setListener(
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
isGone = true
}
},
)
}
}

View File

@@ -21,9 +21,7 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
@@ -132,12 +130,6 @@ class ReaderViewModel @Inject constructor(
valueProducer = { readerAnimation }, valueProducer = { readerAnimation },
) )
val isSliderVisible = settings.observeAsStateFlow(
scope = viewModelScope + Dispatchers.Default,
key = AppSettings.KEY_READER_SLIDER,
valueProducer = { isReaderSliderEnabled },
)
val isInfoBarEnabled = settings.observeAsStateFlow( val isInfoBarEnabled = settings.observeAsStateFlow(
scope = viewModelScope + Dispatchers.Default, scope = viewModelScope + Dispatchers.Default,
key = AppSettings.KEY_READER_BAR, key = AppSettings.KEY_READER_BAR,
@@ -199,10 +191,6 @@ class ReaderViewModel @Inject constructor(
init { init {
loadImpl() loadImpl()
settings.observe()
.onEach { key ->
if (key == AppSettings.KEY_READER_SLIDER) notifyStateChanged()
}.launchIn(viewModelScope + Dispatchers.Default)
launchJob(Dispatchers.Default) { launchJob(Dispatchers.Default) {
val mangaId = manga.filterNotNull().first().id val mangaId = manga.filterNotNull().first().id
appShortcutManager.notifyMangaOpened(mangaId) appShortcutManager.notifyMangaOpened(mangaId)
@@ -220,10 +208,6 @@ class ReaderViewModel @Inject constructor(
} }
} }
fun setSliderVisibility(visible: Boolean) {
settings.isReaderSliderEnabled = visible
}
fun switchMode(newMode: ReaderMode) { fun switchMode(newMode: ReaderMode) {
launchJob { launchJob {
val manga = checkNotNull(getMangaOrNull()) val manga = checkNotNull(getMangaOrNull())
@@ -459,7 +443,6 @@ class ReaderViewModel @Inject constructor(
chaptersTotal = m.chapters[chapter.branch].sizeOrZero(), chaptersTotal = m.chapters[chapter.branch].sizeOrZero(),
totalPages = chaptersLoader.getPagesCount(chapter.id), totalPages = chaptersLoader.getPagesCount(chapter.id),
currentPage = state.page, currentPage = state.page,
isSliderEnabled = settings.isReaderSliderEnabled,
percent = computePercent(state.chapterId, state.page), percent = computePercent(state.chapterId, state.page),
incognito = incognitoMode.value, incognito = incognitoMode.value,
) )

View File

@@ -10,10 +10,11 @@ data class ReaderUiState(
val totalPages: Int, val totalPages: Int,
val percent: Float, val percent: Float,
val incognito: Boolean, val incognito: Boolean,
private val isSliderEnabled: Boolean,
) { ) {
fun isSliderAvailable(): Boolean { fun hasNextChapter(): Boolean = chapterNumber < chaptersTotal
return isSliderEnabled && totalPages > 1 && currentPage < totalPages
} fun hasPreviousChapter(): Boolean = chapterNumber > 1
fun isSliderAvailable(): Boolean = totalPages > 1 && currentPage < totalPages
} }

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:alpha="0.6" android:color="?colorSurfaceBright" />
</selector>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- https://stackoverflow.com/questions/54685474/theme-attributes-in-color-selector-for-api-22 -->
<item android:alpha="0.6" android:color="@color/kotatsu_surfaceBright" />
</selector>

View File

@@ -1,158 +0,0 @@
<?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"
android:keepScreenOn="true">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<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:paddingHorizontal="6dp"
android:paddingTop="8dp"
android:textSize="12sp"
android:visibility="gone"
tools:visibility="visible" />
<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:visibility="gone"
app:layout_dodgeInsetEdges="bottom"
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">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<com.google.android.material.appbar.MaterialToolbar
android:id="@id/toolbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
tools:menu="@menu/opt_reader_top" />
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar_bottom"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
tools:menu="@menu/opt_reader" />
</LinearLayout>
</com.google.android.material.appbar.AppBarLayout>
<com.google.android.material.floatingtoolbar.FloatingToolbarLayout
android:id="@+id/floating_toolbar"
style="@style/Widget.Material3.FloatingToolbar.Vertical.Vibrant"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|center_vertical">
<RelativeLayout
android:id="@+id/floating_toolbar_child"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical">
<com.google.android.material.slider.Slider
android:id="@+id/slider"
android:layout_width="wrap_content"
android:layout_height="240dp"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:orientation="vertical"
android:stepSize="1.0"
android:valueFrom="0"
app:labelBehavior="floating"
tools:value="6"
tools:valueTo="20" />
<ImageButton
android:id="@+id/button_prev"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/slider"
android:layout_centerHorizontal="true"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/prev_chapter"
android:padding="@dimen/margin_small"
android:src="@drawable/ic_prev" />
<ImageButton
android:id="@+id/button_next"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/button_prev"
android:layout_centerHorizontal="true"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/next_chapter"
android:padding="@dimen/margin_small"
android:src="@drawable/ic_next" />
</RelativeLayout>
</com.google.android.material.floatingtoolbar.FloatingToolbarLayout>
<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: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:gravity="center_horizontal"
android:orientation="vertical">
<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="8dp"
android:text="@string/loading_"
android:textAppearance="?attr/textAppearanceBody2" />
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -19,6 +19,7 @@
android:layout_gravity="bottom|end" android:layout_gravity="bottom|end"
android:layout_margin="16dp" android:layout_margin="16dp"
android:orientation="vertical" android:orientation="vertical"
android:spacing="2dp"
android:visibility="gone" android:visibility="gone"
app:layout_dodgeInsetEdges="bottom" app:layout_dodgeInsetEdges="bottom"
tools:visibility="visible" /> tools:visibility="visible" />
@@ -66,62 +67,56 @@
android:id="@+id/toolbar_bottom" android:id="@+id/toolbar_bottom"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
tools:menu="@menu/opt_reader" /> tools:menu="@menu/opt_reader_bottom">
<RelativeLayout
android:id="@+id/layout_slider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="2dp">
<ImageButton
android:id="@+id/button_prev"
style="?actionButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
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="0dp"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/button_next"
android:layout_toEndOf="@id/button_prev"
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:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:contentDescription="@string/next_chapter"
android:src="@drawable/ic_next"
android:tooltipText="@string/next_chapter" />
</RelativeLayout>
</com.google.android.material.appbar.MaterialToolbar>
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>
<com.google.android.material.floatingtoolbar.FloatingToolbarLayout
android:id="@+id/floating_toolbar"
style="@style/Widget.Material3.FloatingToolbar.Horizontal.Vibrant"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_dodgeInsetEdges="bottom">
<RelativeLayout
android:id="@+id/floating_toolbar_child"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical">
<ImageButton
android:id="@+id/button_prev"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/prev_chapter"
android:padding="@dimen/margin_small"
android:src="@drawable/ic_prev" />
<com.google.android.material.slider.Slider
android:id="@+id/slider"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/button_next"
android:layout_toEndOf="@id/button_prev"
android:stepSize="1.0"
android:valueFrom="0"
app:labelBehavior="floating"
tools:value="6"
tools:valueTo="20" />
<ImageButton
android:id="@+id/button_next"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/next_chapter"
android:padding="@dimen/margin_small"
android:src="@drawable/ic_next" />
</RelativeLayout>
</com.google.android.material.floatingtoolbar.FloatingToolbarLayout>
<org.koitharu.kotatsu.reader.ui.ReaderToastView <org.koitharu.kotatsu.reader.ui.ReaderToastView
android:id="@+id/toastView" android:id="@+id/toastView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -130,6 +125,7 @@
android:layout_marginBottom="20dp" android:layout_marginBottom="20dp"
android:background="@drawable/bg_reader_indicator" android:background="@drawable/bg_reader_indicator"
android:drawablePadding="6dp" android:drawablePadding="6dp"
android:elevation="1000dp"
android:singleLine="true" android:singleLine="true"
android:textAppearance="?attr/textAppearanceBodySmall" android:textAppearance="?attr/textAppearanceBodySmall"
android:theme="@style/ThemeOverlay.Material3.Dark" android:theme="@style/ThemeOverlay.Material3.Dark"

View File

@@ -25,6 +25,7 @@
android:clipToPadding="false" android:clipToPadding="false"
android:paddingTop="2dp" android:paddingTop="2dp"
android:paddingBottom="6dp" android:paddingBottom="6dp"
app:chipIconVisible="false"
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter" app:chipStyle="@style/Widget.Kotatsu.Chip.Filter"
app:singleLine="true" /> app:singleLine="true" />

View File

@@ -5,39 +5,28 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:parentTag="android.widget.LinearLayout"> tools:orientation="vertical"
tools:parentTag="com.google.android.material.button.MaterialButtonGroup">
<com.google.android.material.imageview.ShapeableImageView <com.google.android.material.button.MaterialButton
android:id="@+id/button_zoom_in" android:id="@+id/button_zoom_in"
android:layout_width="?minTouchTargetSize" style="@style/Widget.Material3.Button.IconButton.Outlined"
android:layout_height="?minTouchTargetSize" android:layout_width="wrap_content"
android:layout_margin="4dp" android:layout_height="wrap_content"
android:alpha="0.8"
android:background="@drawable/bg_circle_button"
android:contentDescription="@string/zoom_in" android:contentDescription="@string/zoom_in"
android:padding="1dp"
android:scaleType="centerInside"
android:src="@drawable/ic_zoom_in"
android:tooltipText="@string/zoom_in" android:tooltipText="@string/zoom_in"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Circle" app:backgroundTint="@color/bg_floating_button"
app:strokeColor="?colorOutline" app:icon="@drawable/ic_zoom_in" />
app:strokeWidth="1dp" />
<com.google.android.material.imageview.ShapeableImageView <com.google.android.material.button.MaterialButton
android:id="@+id/button_zoom_out" android:id="@+id/button_zoom_out"
android:layout_width="?minTouchTargetSize" style="@style/Widget.Material3.Button.IconButton.Outlined"
android:layout_height="?minTouchTargetSize" android:layout_width="wrap_content"
android:layout_margin="4dp" android:layout_height="wrap_content"
android:alpha="0.8"
android:background="@drawable/bg_circle_button"
android:contentDescription="@string/zoom_out" android:contentDescription="@string/zoom_out"
android:padding="1dp"
android:scaleType="centerInside"
android:src="@drawable/ic_zoom_out"
android:tooltipText="@string/zoom_out" android:tooltipText="@string/zoom_out"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Circle" app:backgroundTint="@color/bg_floating_button"
app:strokeColor="?colorOutline" app:icon="@drawable/ic_zoom_out" />
app:strokeWidth="1dp" />
</merge> </merge>

View File

@@ -5,19 +5,6 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
tools:ignore="AlwaysShowAction"> tools:ignore="AlwaysShowAction">
<item
android:id="@+id/action_slider"
android:icon="@drawable/ic_move_horizontal"
android:title="@string/show_slider"
app:showAsAction="always" />
<item
android:id="@+id/action_bookmark"
android:enabled="false"
android:icon="@drawable/ic_bookmark"
android:title="@string/bookmark_add"
app:showAsAction="always" />
<item <item
android:id="@+id/action_pages_thumbs" android:id="@+id/action_pages_thumbs"
android:icon="@drawable/ic_grid" android:icon="@drawable/ic_grid"

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_bookmark"
android:enabled="false"
android:icon="@drawable/ic_bookmark"
android:title="@string/bookmark_add"
app:showAsAction="always" />
</menu>

View File

@@ -141,6 +141,7 @@
<declare-styleable name="ChipsView"> <declare-styleable name="ChipsView">
<attr name="chipStyle" /> <attr name="chipStyle" />
<attr name="chipIconVisible" />
</declare-styleable> </declare-styleable>
<declare-styleable name="ReaderInfoBarView"> <declare-styleable name="ReaderInfoBarView">

View File

@@ -12,7 +12,7 @@ conscrypt = "2.5.2"
constraintlayout = "2.2.0" constraintlayout = "2.2.0"
coreKtx = "1.15.0" coreKtx = "1.15.0"
coroutines = "1.9.0" coroutines = "1.9.0"
desugar = "2.1.3" desugar = "2.1.4"
diskLruCache = "1.4" diskLruCache = "1.4"
fragment = "1.8.5" fragment = "1.8.5"
gradle = "8.7.3" gradle = "8.7.3"
@@ -27,7 +27,7 @@ ksp = "2.0.21-1.0.28"
leakcanary = "3.0-alpha-8" leakcanary = "3.0-alpha-8"
lifecycle = "2.8.7" lifecycle = "2.8.7"
markwon = "4.6.2" markwon = "4.6.2"
material = "1.13.0-alpha08" material = "1.13.0-alpha09"
moshi = "1.15.2" moshi = "1.15.2"
okhttp = "4.12.0" okhttp = "4.12.0"
okio = "3.9.1" okio = "3.9.1"