Improve reader scroll timer

This commit is contained in:
Koitharu
2025-04-28 19:06:24 +03:00
parent 1a8045b89f
commit c871255eb7
16 changed files with 346 additions and 108 deletions

View File

@@ -4,7 +4,7 @@ import java.util.EnumSet
enum class ReaderControl {
PREV_CHAPTER, NEXT_CHAPTER, SLIDER, PAGES_SHEET, SCREEN_ROTATION, SAVE_PAGE;
PREV_CHAPTER, NEXT_CHAPTER, SLIDER, PAGES_SHEET, SCREEN_ROTATION, SAVE_PAGE, TIMER;
companion object {

View File

@@ -10,6 +10,7 @@ import androidx.appcompat.widget.ActionMenuView
import androidx.appcompat.widget.Toolbar
import androidx.core.view.children
import androidx.core.view.descendants
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.recyclerview.widget.RecyclerView
@@ -34,6 +35,9 @@ fun View.hasGlobalPoint(x: Int, y: Int): Boolean {
return rect.contains(x, y)
}
val ViewGroup.hasVisibleChildren: Boolean
get() = children.any { it.isVisible }
fun View.measureHeight(): Int {
val vh = height
return if (vh == 0) {

View File

@@ -22,6 +22,7 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ReaderControl
import org.koitharu.kotatsu.core.util.ext.hasVisibleChildren
import org.koitharu.kotatsu.core.util.ext.isRtl
import org.koitharu.kotatsu.core.util.ext.setValueRounded
import org.koitharu.kotatsu.databinding.LayoutReaderActionsBinding
@@ -83,6 +84,7 @@ class ReaderActionsView @JvmOverloads constructor(
binding.buttonOptions.initAction()
binding.buttonScreenRotation.initAction()
binding.buttonPagesThumbs.initAction()
binding.buttonTimer.initAction()
binding.slider.setLabelFormatter(PageLabelFormatter())
binding.slider.addOnChangeListener(this)
binding.slider.addOnSliderTouchListener(this)
@@ -110,6 +112,7 @@ class ReaderActionsView @JvmOverloads constructor(
R.id.button_prev -> listener?.switchChapterBy(-1)
R.id.button_next -> listener?.switchChapterBy(1)
R.id.button_save -> listener?.onSavePageClick()
R.id.button_timer -> listener?.onScrollTimerClick()
R.id.button_pages_thumbs -> AppRouter.from(this)?.showChapterPagesSheet()
R.id.button_screen_rotation -> listener?.toggleScreenOrientation()
R.id.button_options -> listener?.openMenu()
@@ -158,6 +161,12 @@ class ReaderActionsView @JvmOverloads constructor(
binding.slider.isRtl = reversed != isRtl
}
fun setTimerActive(isActive: Boolean) {
binding.buttonTimer.setIconResource(
if (isActive) R.drawable.ic_timer_run else R.drawable.ic_timer,
)
}
private fun updateControlsVisibility() {
val controls = settings.readerControls
binding.buttonPrev.isVisible = ReaderControl.PREV_CHAPTER in controls
@@ -165,6 +174,7 @@ class ReaderActionsView @JvmOverloads constructor(
binding.buttonPagesThumbs.isVisible = ReaderControl.PAGES_SHEET in controls
binding.buttonScreenRotation.isVisible = ReaderControl.SCREEN_ROTATION in controls
binding.buttonSave.isVisible = ReaderControl.SAVE_PAGE in controls
binding.buttonTimer.isVisible = ReaderControl.TIMER in controls
binding.slider.isVisible = ReaderControl.SLIDER in controls
adjustLayoutParams()
}
@@ -185,6 +195,7 @@ class ReaderActionsView @JvmOverloads constructor(
repeat(childCount) { i ->
val child = getChildAt(i)
if (child is FrameLayout) {
child.isVisible = child.hasVisibleChildren
child.updateLayoutParams<LayoutParams> {
width = if (isSliderVisible) LayoutParams.WRAP_CONTENT else 0
weight = if (isSliderVisible) 0f else 1f

View File

@@ -87,12 +87,6 @@ class ReaderActivity :
override val readerMode: ReaderMode?
get() = readerManager.currentMode
override var isAutoScrollEnabled: Boolean
get() = scrollTimer.isEnabled
set(value) {
scrollTimer.isEnabled = value
}
private lateinit var scrollTimer: ScrollTimer
private lateinit var pageSaveHelper: PageSaveHelper
private lateinit var touchHelper: TapGridDispatcher
@@ -107,13 +101,15 @@ class ReaderActivity :
readerManager = ReaderManager(supportFragmentManager, viewBinding.container, settings)
setDisplayHomeAsUp(isEnabled = true, showUpAsClose = false)
touchHelper = TapGridDispatcher(this, this)
scrollTimer = scrollTimerFactory.create(this, this)
scrollTimer = scrollTimerFactory.create(resources, this, this)
pageSaveHelper = pageSaveHelperFactory.create(this)
controlDelegate = ReaderControlDelegate(resources, settings, tapGridSettings, this)
viewBinding.zoomControl.listener = this
viewBinding.actionsView.listener = this
idlingDetector.bindToLifecycle(this)
screenOrientationHelper.applySettings()
scrollTimer.isActive.observe(this) { viewBinding.actionsView.setTimerActive(it) }
viewBinding.timerControl.attach(scrollTimer, this)
viewModel.onError.observeEvent(
this,
@@ -162,7 +158,9 @@ class ReaderActivity :
override fun onUserInteraction() {
super.onUserInteraction()
scrollTimer.onUserInteraction()
if (!viewBinding.timerControl.isVisible) {
scrollTimer.onUserInteraction()
}
idlingDetector.onUserInteraction()
}
@@ -196,6 +194,7 @@ class ReaderActivity :
lifecycle.postDelayed(TimeUnit.SECONDS.toMillis(1), hideUiRunnable)
}
viewBinding.actionsView.setSliderReversed(mode == ReaderMode.REVERSED)
viewBinding.timerControl.onReaderModeChanged(mode)
}
private fun onLoadingStateChanged(isLoading: Boolean) {
@@ -237,7 +236,9 @@ class ReaderActivity :
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
touchHelper.dispatchTouchEvent(ev)
scrollTimer.onTouchEvent(ev)
if (!viewBinding.timerControl.hasGlobalPoint(ev.rawX.toInt(), ev.rawY.toInt())) {
scrollTimer.onTouchEvent(ev)
}
return super.dispatchTouchEvent(ev)
}
@@ -272,6 +273,7 @@ class ReaderActivity :
override fun onReaderModeChanged(mode: ReaderMode) {
viewModel.saveCurrentState(readerManager.currentReader?.getCurrentState())
viewModel.switchMode(mode)
viewBinding.timerControl.onReaderModeChanged(mode)
}
override fun onDoubleModeChanged(isEnabled: Boolean) {
@@ -361,6 +363,10 @@ class ReaderActivity :
viewModel.saveCurrentPage(pageSaveHelper)
}
override fun onScrollTimerClick() {
viewBinding.timerControl.showOrHide()
}
override fun toggleScreenOrientation() {
if (screenOrientationHelper.toggleScreenOrientation()) {
Snackbar.make(

View File

@@ -142,6 +142,8 @@ class ReaderControlDelegate(
fun onSavePageClick()
fun onScrollTimerClick()
fun toggleScreenOrientation()
fun isReaderResumed(): Boolean

View File

@@ -1,5 +1,7 @@
package org.koitharu.kotatsu.reader.ui
import android.content.res.Resources
import android.os.SystemClock
import android.view.MotionEvent
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
@@ -10,6 +12,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
@@ -19,6 +22,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.yield
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.util.ext.resolveDp
import kotlin.math.roundToLong
private const val MAX_DELAY = 8L
@@ -27,6 +31,7 @@ private const val INTERACTION_SKIP_MS = 2_000L
private const val SPEED_FACTOR_DELTA = 0.02f
class ScrollTimer @AssistedInject constructor(
@Assisted resources: Resources,
@Assisted private val listener: ReaderControlDelegate.OnInteractionListener,
@Assisted lifecycleOwner: LifecycleOwner,
settings: AppSettings,
@@ -35,17 +40,15 @@ class ScrollTimer @AssistedInject constructor(
private val coroutineScope = lifecycleOwner.lifecycleScope
private var job: Job? = null
private var delayMs: Long = 10L
private var pageSwitchDelay: Long = 100L
var pageSwitchDelay: Long = 100L
private set
private var resumeAt = 0L
private var isTouchDown = MutableStateFlow(false)
private val isRunning = MutableStateFlow(false)
private val scrollDelta = resources.resolveDp(2)
var isEnabled: Boolean = false
set(value) {
if (field != value) {
field = value
restartJob()
}
}
val isActive: StateFlow<Boolean>
get() = isRunning
init {
settings.observeAsFlow(AppSettings.KEY_READER_AUTOSCROLL_SPEED) {
@@ -56,8 +59,15 @@ class ScrollTimer @AssistedInject constructor(
}.launchIn(coroutineScope)
}
fun setActive(value: Boolean) {
if (isRunning.value != value) {
isRunning.value = value
restartJob()
}
}
fun onUserInteraction() {
resumeAt = System.currentTimeMillis() + INTERACTION_SKIP_MS
resumeAt = SystemClock.elapsedRealtime() + INTERACTION_SKIP_MS
}
fun onTouchEvent(event: MotionEvent) {
@@ -90,7 +100,7 @@ class ScrollTimer @AssistedInject constructor(
private fun restartJob() {
job?.cancel()
resumeAt = 0L
if (!isEnabled || delayMs == 0L) {
if (!isRunning.value || delayMs == 0L) {
job = null
return
}
@@ -114,7 +124,7 @@ class ScrollTimer @AssistedInject constructor(
if (!listener.isReaderResumed()) {
continue
}
if (!listener.scrollBy(1, false)) {
if (!listener.scrollBy(scrollDelta, false)) {
accumulator += delayMs
}
if (accumulator >= pageSwitchDelay) {
@@ -126,12 +136,12 @@ class ScrollTimer @AssistedInject constructor(
}
private fun isPaused(): Boolean {
return isTouchDown.value || resumeAt > System.currentTimeMillis()
return isTouchDown.value || resumeAt > SystemClock.elapsedRealtime()
}
private suspend fun delayUntilResumed() {
while (isPaused()) {
val delayTime = resumeAt - System.currentTimeMillis()
val delayTime = resumeAt - SystemClock.elapsedRealtime()
if (delayTime > 0) {
delay(delayTime)
} else {
@@ -145,6 +155,7 @@ class ScrollTimer @AssistedInject constructor(
interface Factory {
fun create(
resources: Resources,
lifecycleOwner: LifecycleOwner,
listener: ReaderControlDelegate.OnInteractionListener,
): ScrollTimer

View File

@@ -0,0 +1,142 @@
package org.koitharu.kotatsu.reader.ui
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.CompoundButton
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.transition.Slide
import androidx.transition.TransitionManager
import com.google.android.material.slider.LabelFormatter
import com.google.android.material.slider.Slider
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.parentView
import org.koitharu.kotatsu.databinding.ViewScrollTimerBinding
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@AndroidEntryPoint
class ScrollTimerControlView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : ConstraintLayout(context, attrs), CompoundButton.OnCheckedChangeListener, Slider.OnChangeListener,
View.OnClickListener, LabelFormatter {
@Inject
lateinit var settings: AppSettings
private val binding = ViewScrollTimerBinding.inflate(LayoutInflater.from(context), this)
private var scrollTimer: ScrollTimer? = null
private var labelPattern = context.getString(R.string.speed_value)
private var readerMode: ReaderMode = ReaderMode.STANDARD
init {
binding.switchScrollTimer.setOnCheckedChangeListener(this)
binding.sliderTimer.addOnChangeListener(this)
binding.sliderTimer.setLabelFormatter(this)
binding.buttonClose.setOnClickListener(this)
setPadding(0, 0, 0, context.resources.getDimensionPixelOffset(R.dimen.margin_normal))
}
fun attach(timer: ScrollTimer, lifecycleOwner: LifecycleOwner) {
scrollTimer = timer
timer.isActive.observe(lifecycleOwner) {
binding.switchScrollTimer.setOnCheckedChangeListener(null)
binding.switchScrollTimer.isChecked = it
binding.switchScrollTimer.setOnCheckedChangeListener(this)
}
settings.observeAsStateFlow(
scope = lifecycleOwner.lifecycleScope + Dispatchers.Default,
key = AppSettings.KEY_READER_AUTOSCROLL_SPEED,
valueProducer = { readerAutoscrollSpeed },
).observe(lifecycleOwner) {
binding.sliderTimer.value = it.coerceIn(
binding.sliderTimer.valueFrom,
binding.sliderTimer.valueTo,
)
}
updateDescription()
}
fun onReaderModeChanged(mode: ReaderMode) {
readerMode = mode
updateDescription()
}
override fun onClick(v: View) {
when (v.id) {
R.id.button_close -> hide()
}
}
override fun getFormattedValue(value: Float): String {
// val minValue = binding.sliderTimer.valueFrom
// val maxValue = binding.sliderTimer.valueTo
return labelPattern.format(value * 10f)
}
override fun onValueChange(
slider: Slider,
value: Float,
fromUser: Boolean
) {
if (fromUser) {
settings.readerAutoscrollSpeed = value
}
updateDescription()
}
override fun onCheckedChanged(buttonView: CompoundButton?, isChecked: Boolean) {
scrollTimer?.setActive(isChecked)
}
fun show() {
setupVisibilityTransition()
isVisible = true
}
fun hide() {
setupVisibilityTransition()
isVisible = false
}
fun showOrHide() {
setupVisibilityTransition()
isVisible = !isVisible
}
private fun setupVisibilityTransition() {
if (context.isAnimationsEnabled) {
val sceneRoot = parentView ?: return
val transition = Slide()
transition.addTarget(this)
TransitionManager.beginDelayedTransition(sceneRoot, transition)
}
}
private fun updateDescription() {
val timePerPage = scrollTimer?.pageSwitchDelay ?: 0L
if (timePerPage <= 0L || readerMode == ReaderMode.WEBTOON) {
binding.textViewDescription.isVisible = false
} else {
binding.textViewDescription.text = context.getString(
R.string.page_switch_timer,
TimeUnit.MILLISECONDS.toSeconds((scrollTimer ?: return).pageSwitchDelay),
)
binding.textViewDescription.isVisible = true
}
}
}

View File

@@ -10,26 +10,20 @@ import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import com.google.android.material.button.MaterialButtonToggleGroup
import com.google.android.material.slider.Slider
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.nav.router
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
import org.koitharu.kotatsu.core.util.ext.consume
import org.koitharu.kotatsu.core.util.ext.findParentCallback
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
import org.koitharu.kotatsu.databinding.SheetReaderConfigBinding
import org.koitharu.kotatsu.reader.domain.PageLoader
@@ -42,7 +36,6 @@ class ReaderConfigSheet :
BaseAdaptiveSheet<SheetReaderConfigBinding>(),
View.OnClickListener,
MaterialButtonToggleGroup.OnButtonCheckedListener,
Slider.OnChangeListener,
CompoundButton.OnCheckedChangeListener {
private val viewModel by activityViewModels<ReaderViewModel>()
@@ -99,8 +92,7 @@ class ReaderConfigSheet :
binding.buttonSettings.setOnClickListener(this)
binding.buttonImageServer.setOnClickListener(this)
binding.buttonColorFilter.setOnClickListener(this)
binding.sliderTimer.addOnChangeListener(this)
binding.switchScrollTimer.setOnCheckedChangeListener(this)
binding.buttonScrollTimer.setOnClickListener(this)
binding.switchDoubleReader.setOnCheckedChangeListener(this)
viewLifecycleScope.launch {
@@ -110,20 +102,6 @@ class ReaderConfigSheet :
}
binding.buttonImageServer.isVisible = isAvailable
}
settings.observeAsStateFlow(
scope = lifecycleScope + Dispatchers.Default,
key = AppSettings.KEY_READER_AUTOSCROLL_SPEED,
valueProducer = { readerAutoscrollSpeed },
).observe(viewLifecycleOwner) {
binding.sliderTimer.value = it.coerceIn(
binding.sliderTimer.valueFrom,
binding.sliderTimer.valueTo,
)
}
findParentCallback(Callback::class.java)?.run {
binding.switchScrollTimer.isChecked = isAutoScrollEnabled
}
}
override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
@@ -141,6 +119,11 @@ class ReaderConfigSheet :
dismissAllowingStateLoss()
}
R.id.button_scroll_timer -> {
findParentCallback(Callback::class.java)?.onScrollTimerClick() ?: return
dismissAllowingStateLoss()
}
R.id.button_save_page -> {
findParentCallback(Callback::class.java)?.onSavePageClick() ?: return
dismissAllowingStateLoss()
@@ -168,12 +151,6 @@ class ReaderConfigSheet :
override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
when (buttonView.id) {
R.id.switch_scroll_timer -> {
findParentCallback(Callback::class.java)?.isAutoScrollEnabled = isChecked
requireViewBinding().layoutTimer.isVisible = isChecked
requireViewBinding().sliderTimer.isVisible = isChecked
}
R.id.switch_screen_lock_rotation -> {
orientationHelper.isLocked = isChecked
}
@@ -208,13 +185,6 @@ class ReaderConfigSheet :
mode = newMode
}
override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) {
if (fromUser) {
settings.readerAutoscrollSpeed = value
}
(viewBinding ?: return).labelTimerValue.text = getString(R.string.speed_value, value * 10f)
}
private fun observeScreenOrientation() {
orientationHelper.observeAutoOrientation()
.onEach {
@@ -243,12 +213,12 @@ class ReaderConfigSheet :
interface Callback {
var isAutoScrollEnabled: Boolean
fun onReaderModeChanged(mode: ReaderMode)
fun onDoubleModeChanged(isEnabled: Boolean)
fun onSavePageClick()
fun onScrollTimerClick()
}
}

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M15 3H9V1H15V3M11 14H13V8H11V14M19 13C19.7 13 20.36 13.13 21 13.35C21 13.23 21 13.12 21 13C21 10.88 20.26 8.93 19.03 7.39L20.45 5.97C20 5.46 19.55 5 19.04 4.56L17.62 6C16.07 4.74 14.12 4 12 4C7.03 4 3 8.03 3 13S7.03 22 12 22C12.59 22 13.16 21.94 13.71 21.83C13.4 21.25 13.18 20.6 13.08 19.91C12.72 19.96 12.37 20 12 20C8.13 20 5 16.87 5 13S8.13 6 12 6 19 9.13 19 13M17 16V22L22 19L17 16Z" />
</vector>

View File

@@ -34,6 +34,17 @@
android:visibility="gone"
tools:visibility="visible" />
<org.koitharu.kotatsu.reader.ui.ScrollTimerControlView
android:id="@+id/timerControl"
android:layout_width="320dp"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
android:layout_margin="@dimen/screen_padding"
android:background="@drawable/bg_card"
android:elevation="@dimen/m3_card_elevated_elevation"
android:visibility="gone"
app:layout_dodgeInsetEdges="top" />
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar_top"
android:layout_width="match_parent"

View File

@@ -34,6 +34,17 @@
android:visibility="gone"
tools:visibility="visible" />
<org.koitharu.kotatsu.reader.ui.ScrollTimerControlView
android:id="@+id/timerControl"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_margin="@dimen/screen_padding"
android:background="@drawable/bg_card"
android:elevation="@dimen/m3_card_elevated_elevation"
android:visibility="gone"
app:layout_dodgeInsetEdges="bottom" />
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar_top"
android:layout_width="match_parent"

View File

@@ -47,7 +47,7 @@
</FrameLayout>
<FrameLayout
android:layout_width="0dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<com.google.android.material.button.MaterialButton
@@ -59,6 +59,19 @@
app:icon="@drawable/ic_save" />
</FrameLayout>
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<com.google.android.material.button.MaterialButton
android:id="@+id/button_timer"
style="@style/Widget.Kotatsu.IconButton.Action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/save_page"
app:icon="@drawable/ic_timer" />
</FrameLayout>
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">

View File

@@ -146,61 +146,17 @@
android:textColor="?colorOnSurfaceVariant"
app:drawableStartCompat="@drawable/ic_split_horizontal" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_scroll_timer"
<org.koitharu.kotatsu.core.ui.widgets.ListItemTextView
android:id="@+id/button_scroll_timer"
android:layout_width="match_parent"
android:layout_height="?android:listPreferredItemHeightSmall"
android:drawablePadding="?android:listPreferredItemPaddingStart"
android:ellipsize="end"
android:paddingStart="?android:listPreferredItemPaddingStart"
android:paddingEnd="?android:listPreferredItemPaddingEnd"
android:singleLine="true"
android:text="@string/automatic_scroll"
android:textAppearance="?attr/textAppearanceButton"
android:textColor="?colorOnSurfaceVariant"
app:drawableStartCompat="@drawable/ic_timer" />
<LinearLayout
android:id="@+id/layout_timer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/margin_normal"
android:layout_marginTop="@dimen/margin_normal"
android:textAppearance="?textAppearanceTitleSmall"
android:visibility="gone"
tools:visibility="visible">
<TextView
android:id="@+id/label_timer"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/speed"
android:textAppearance="?attr/textAppearanceTitleSmall" />
<TextView
android:id="@+id/label_timer_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_small"
android:textAppearance="?attr/textAppearanceBodySmall"
tools:text="x0.5" />
</LinearLayout>
<org.koitharu.kotatsu.core.ui.widgets.CubicSlider
android:id="@+id/slider_timer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/margin_normal"
android:contentDescription="@string/automatic_scroll"
android:labelFor="@id/switch_scroll_timer"
android:valueFrom="0.2"
android:valueTo="0.9"
android:visibility="gone"
app:labelBehavior="gone"
tools:visibility="visible" />
<org.koitharu.kotatsu.core.ui.widgets.ListItemTextView
android:id="@+id/button_color_filter"
android:layout_width="match_parent"

View File

@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="utf-8"?>
<merge
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"
tools:background="@drawable/bg_card"
tools:layout_height="wrap_content"
tools:layout_margin="@dimen/screen_padding"
tools:layout_width="match_parent"
tools:paddingBottom="@dimen/margin_normal"
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
<TextView
android:id="@+id/textView_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="?minTouchTargetSize"
android:paddingHorizontal="@dimen/margin_normal"
android:text="@string/automatic_scroll"
android:textAppearance="?textAppearanceTitleMedium"
app:layout_constraintEnd_toStartOf="@id/button_close"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageButton
android:id="@+id/button_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/close"
android:minWidth="?minTouchTargetSize"
android:minHeight="?minTouchTargetSize"
android:src="?actionModeCloseDrawable"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_scroll_timer"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/margin_normal"
android:ellipsize="end"
android:singleLine="true"
android:text="@string/enable"
android:textAppearance="?attr/textAppearanceTitleSmall"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textView_title" />
<TextView
android:id="@+id/label_timer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_normal"
android:text="@string/speed"
android:textAppearance="?attr/textAppearanceTitleSmall"
app:layout_constraintBottom_toBottomOf="@id/slider_timer"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/slider_timer" />
<org.koitharu.kotatsu.core.ui.widgets.CubicSlider
android:id="@+id/slider_timer"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/margin_normal"
android:contentDescription="@string/speed"
android:labelFor="@id/switch_scroll_timer"
android:valueFrom="0.001"
android:valueTo="0.9"
app:labelBehavior="floating"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/label_timer"
app:layout_constraintTop_toBottomOf="@id/switch_scroll_timer" />
<TextView
android:id="@+id/textView_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/margin_normal"
android:layout_marginTop="@dimen/margin_small"
android:textAppearance="?textAppearanceBodySmall"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/slider_timer"
tools:text="@string/page_switch_timer" />
</merge>

View File

@@ -131,6 +131,7 @@
<item>@string/chapters_and_pages</item>
<item>@string/screen_orientation</item>
<item>@string/save_page</item>
<item>@string/automatic_scroll</item>
</string-array>
<string-array name="list_badges" translatable="false">
<item>@string/favourites</item>

View File

@@ -831,4 +831,5 @@
<string name="pick_manga_page">Pick manga page</string>
<string name="pick_custom_file">Pick custom file</string>
<string name="change_cover">Change cover</string>
<string name="page_switch_timer">The page will switch every ~%d seconds</string>
</resources>