Refactor scroll timer

This commit is contained in:
Koitharu
2023-03-24 15:03:40 +02:00
parent bc4dd1c507
commit 43d55cedae
8 changed files with 163 additions and 159 deletions

View File

@@ -4,6 +4,7 @@ import android.content.Context
import android.content.SharedPreferences
import android.net.Uri
import android.provider.Settings
import androidx.annotation.FloatRange
import androidx.appcompat.app.AppCompatDelegate
import androidx.collection.arraySetOf
import androidx.core.content.edit
@@ -265,6 +266,11 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val isWebtoonZoomEnable: Boolean
get() = prefs.getBoolean(KEY_WEBTOON_ZOOM, true)
@get:FloatRange(from = 0.0, to = 1.0)
var readerAutoscrollSpeed: Float
get() = prefs.getFloat(KEY_READER_AUTOSCROLL_SPEED, 0f)
set(@FloatRange(from = 0.0, to = 1.0) value) = prefs.edit { putFloat(KEY_READER_AUTOSCROLL_SPEED, value) }
fun isPagesPreloadEnabled(): Boolean {
val policy = NetworkPolicy.from(prefs.getString(KEY_PAGES_PRELOAD, null), NetworkPolicy.NON_METERED)
return policy.isNetworkAllowed(connectivityManager)
@@ -397,6 +403,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_UPDATES_UNSTABLE = "updates_unstable"
const val KEY_TIPS_CLOSED = "tips_closed"
const val KEY_SSL_BYPASS = "ssl_bypass"
const val KEY_READER_AUTOSCROLL_SPEED = "as_speed"
// About
const val KEY_APP_UPDATE = "app_update"

View File

@@ -53,6 +53,7 @@ import org.koitharu.kotatsu.utils.ext.observeWithPrevious
import org.koitharu.kotatsu.utils.ext.postDelayed
import org.koitharu.kotatsu.utils.ext.setValueRounded
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@AndroidEntryPoint
class ReaderActivity :
@@ -69,15 +70,18 @@ class ReaderActivity :
private val viewModel: ReaderViewModel by viewModels()
override var autoScrollSpeed: Float
get() = scrollTimer.speed
set(value) {
scrollTimer.speed = value
}
override val readerMode: ReaderMode?
get() = readerManager.currentMode
override var isAutoScrollEnabled: Boolean
get() = scrollTimer.isEnabled
set(value) {
scrollTimer.isEnabled = value
}
@Inject
lateinit var scrollTimerFactory: ScrollTimer.Factory
private lateinit var scrollTimer: ScrollTimer
private lateinit var touchHelper: GridTouchHelper
private lateinit var controlDelegate: ReaderControlDelegate
@@ -91,7 +95,7 @@ class ReaderActivity :
readerManager = ReaderManager(supportFragmentManager, R.id.container)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
touchHelper = GridTouchHelper(this, this)
scrollTimer = ScrollTimer(this, this)
scrollTimer = scrollTimerFactory.create(this, this)
controlDelegate = ReaderControlDelegate(settings, this, this)
binding.toolbarBottom.setOnMenuItemClickListener(::onOptionsItemSelected)
binding.slider.setLabelFormatter(PageLabelFormatter())
@@ -133,6 +137,7 @@ class ReaderActivity :
override fun onUserInteraction() {
super.onUserInteraction()
scrollTimer.onUserInteraction()
idlingDetector.onUserInteraction()
}
@@ -343,6 +348,11 @@ class ReaderActivity :
setUiIsVisible(!binding.appbarTop.isVisible)
}
override fun isReaderResumed(): Boolean {
val reader = readerManager.currentReader ?: return false
return reader.isResumed && supportFragmentManager.fragments.lastOrNull() === reader
}
private fun onReaderBarChanged(isBarEnabled: Boolean) {
binding.infoBar.isVisible = isBarEnabled && binding.appbarTop.isGone
}

View File

@@ -142,5 +142,7 @@ class ReaderControlDelegate(
fun scrollBy(delta: Int): Boolean
fun toggleUiVisibility()
fun isReaderResumed(): Boolean
}
}

View File

@@ -1,44 +1,65 @@
package org.koitharu.kotatsu.reader.ui
import androidx.annotation.FloatRange
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.coroutineScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.lifecycleScope
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import kotlin.math.roundToLong
private const val MIN_SPEED = 0.1
private const val MAX_DELAY = 80L
private const val MAX_SWITCH_DELAY = 20_000L
private const val MAX_DELAY = 60L
private const val MAX_SWITCH_DELAY = 12_000L
private const val INTERACTION_SKIP_MS = 1_000L
class ScrollTimer(
private val lifecycleOwner: LifecycleOwner,
private val listener: ReaderControlDelegate.OnInteractionListener,
class ScrollTimer @AssistedInject constructor(
@Assisted private val listener: ReaderControlDelegate.OnInteractionListener,
@Assisted lifecycleOwner: LifecycleOwner,
settings: AppSettings,
) {
private val coroutineScope = lifecycleOwner.lifecycleScope
private var job: Job? = null
private var delayMs: Long = 10L
private var pageSwitchDelay: Long = 100L
private var skip = 0L
@FloatRange(from = 0.0, to = 1.0)
var speed: Float = 0f
var isEnabled: Boolean = false
set(value) {
if (field != value) {
field = value
onSpeedChanged()
restartJob()
}
}
private fun onSpeedChanged() {
if (speed < MIN_SPEED) {
init {
settings.observeAsFlow(AppSettings.KEY_READER_AUTOSCROLL_SPEED) {
readerAutoscrollSpeed
}.flowOn(Dispatchers.Default)
.onEach {
onSpeedChanged(it)
}.launchIn(coroutineScope)
}
fun onUserInteraction() {
skip = INTERACTION_SKIP_MS
}
private fun onSpeedChanged(speed: Float) {
if (speed <= 0f) {
delayMs = 0L
pageSwitchDelay = 0L
} else {
val speedFactor = 1 - speed + MIN_SPEED
val speedFactor = 1 - speed
delayMs = (MAX_DELAY * speedFactor).roundToLong()
pageSwitchDelay = (MAX_SWITCH_DELAY * speedFactor).roundToLong()
}
@@ -49,24 +70,39 @@ class ScrollTimer(
private fun restartJob() {
job?.cancel()
if (delayMs == 0L) {
skip = 0
if (!isEnabled || delayMs == 0L) {
job = null
return
}
job = lifecycleOwner.lifecycle.coroutineScope.launch {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
var accumulator = 0L
while (isActive) {
delay(delayMs)
if (!listener.scrollBy(1)) {
accumulator += delayMs
}
if (accumulator >= pageSwitchDelay) {
listener.switchPageBy(1)
accumulator -= pageSwitchDelay
}
job = coroutineScope.launch {
var accumulator = 0L
while (isActive) {
delay(delayMs)
if (!listener.isReaderResumed()) {
continue
}
skip -= delayMs
if (skip > 0) {
continue
}
if (!listener.scrollBy(1)) {
accumulator += delayMs
}
if (accumulator >= pageSwitchDelay) {
listener.switchPageBy(1)
accumulator -= pageSwitchDelay
}
}
}
}
@AssistedFactory
interface Factory {
fun create(
lifecycleOwner: LifecycleOwner,
listener: ReaderControlDelegate.OnInteractionListener,
): ScrollTimer
}
}

View File

@@ -1,75 +0,0 @@
package org.koitharu.kotatsu.reader.ui.config
import android.content.res.Resources
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.coroutineScope
import androidx.lifecycle.repeatOnLifecycle
import com.google.android.material.slider.LabelFormatter
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.parsers.util.format
import org.koitharu.kotatsu.reader.ui.ReaderControlDelegate
import kotlin.math.roundToLong
@Deprecated("")
class PageSwitchTimer(
private val listener: ReaderControlDelegate.OnInteractionListener,
private val lifecycleOwner: LifecycleOwner,
) {
var delaySec: Float = 0f
set(value) {
field = value
delayMs = mapDelay(value)
restartJob()
}
private var delayMs = 0L
fun onUserInteraction() {
restartJob()
}
private var job: Job? = null
private fun restartJob() {
job?.cancel()
if (delayMs == 0L) {
job = null
return
}
job = lifecycleOwner.lifecycle.coroutineScope.launch {
// FIXME: pause when bs is opened
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
while (isActive) {
delay(delayMs)
listener.switchPageBy(1)
}
}
}
}
class DelayLabelFormatter(resources: Resources) : LabelFormatter {
private val textOff = resources.getString(R.string.off_short)
private val textSec = resources.getString(R.string.seconds_pattern)
override fun getFormattedValue(value: Float): String {
val ms = mapDelay(value)
return if (ms == 0L) textOff else textSec.format((ms / 1000.0).format(1))
}
}
companion object {
private const val DELAY_MIN = 2000L
fun mapDelay(value: Float): Long {
val delay = (value * 1000L).roundToLong()
return if (delay < DELAY_MIN) 0L else delay
}
}
}

View File

@@ -5,19 +5,25 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CompoundButton
import androidx.activity.result.ActivityResultCallback
import androidx.annotation.FloatRange
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.flowWithLifecycle
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 org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseBottomSheet
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.core.prefs.observeAsLiveData
import org.koitharu.kotatsu.databinding.SheetReaderConfigBinding
import org.koitharu.kotatsu.reader.ui.PageSaveContract
import org.koitharu.kotatsu.reader.ui.ReaderViewModel
@@ -26,19 +32,24 @@ import org.koitharu.kotatsu.settings.SettingsActivity
import org.koitharu.kotatsu.utils.ScreenOrientationHelper
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
import org.koitharu.kotatsu.utils.ext.withArgs
import javax.inject.Inject
@AndroidEntryPoint
class ReaderConfigBottomSheet :
BaseBottomSheet<SheetReaderConfigBinding>(),
ActivityResultCallback<Uri?>,
View.OnClickListener,
MaterialButtonToggleGroup.OnButtonCheckedListener,
Slider.OnChangeListener {
Slider.OnChangeListener, CompoundButton.OnCheckedChangeListener {
private val viewModel by activityViewModels<ReaderViewModel>()
private val savePageRequest = registerForActivityResult(PageSaveContract(), this)
private var orientationHelper: ScreenOrientationHelper? = null
private lateinit var mode: ReaderMode
@Inject
lateinit var settings: AppSettings
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mode = arguments?.getInt(ARG_MODE)
@@ -63,9 +74,17 @@ class ReaderConfigBottomSheet :
binding.buttonSettings.setOnClickListener(this)
binding.buttonColorFilter.setOnClickListener(this)
binding.sliderTimer.addOnChangeListener(this)
binding.switchScrollTimer.setOnCheckedChangeListener(this)
settings.observeAsLiveData(
context = lifecycleScope.coroutineContext + Dispatchers.Default,
key = AppSettings.KEY_READER_AUTOSCROLL_SPEED,
valueProducer = { readerAutoscrollSpeed },
).observe(viewLifecycleOwner) {
binding.sliderTimer.value = it
}
findCallback()?.run {
binding.sliderTimer.value = autoScrollSpeed
binding.switchScrollTimer.isChecked = isAutoScrollEnabled
}
}
@@ -93,6 +112,16 @@ class ReaderConfigBottomSheet :
}
}
override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
when (buttonView.id) {
R.id.switch_scroll_timer -> {
findCallback()?.isAutoScrollEnabled = isChecked
binding.labelTimer.isVisible = isChecked
binding.sliderTimer.isVisible = isChecked
}
}
}
override fun onButtonChecked(group: MaterialButtonToggleGroup?, checkedId: Int, isChecked: Boolean) {
if (!isChecked) {
return
@@ -111,7 +140,9 @@ class ReaderConfigBottomSheet :
}
override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) {
findCallback()?.autoScrollSpeed = value
if (fromUser) {
settings.readerAutoscrollSpeed = value
}
}
override fun onActivityResult(uri: Uri?) {
@@ -135,8 +166,7 @@ class ReaderConfigBottomSheet :
interface Callback {
@get:FloatRange(from = 0.0, to = 1.0)
var autoScrollSpeed: Float
var isAutoScrollEnabled: Boolean
fun onReaderModeChanged(mode: ReaderMode)
}

View File

@@ -103,51 +103,44 @@
android:text="@string/reader_mode_hint"
android:textAppearance="?attr/textAppearanceBodySmall" />
<androidx.constraintlayout.widget.ConstraintLayout
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_scroll_timer"
android:layout_width="match_parent"
android:layout_height="?android:listPreferredItemHeightSmall"
android:layout_marginTop="@dimen/margin_normal"
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" />
<TextView
android:id="@+id/label_timer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_normal"
android:baselineAligned="false"
android:gravity="center_vertical"
android:minHeight="?android:listPreferredItemHeightSmall"
android:orientation="horizontal"
android:paddingStart="?android:listPreferredItemPaddingStart"
android:paddingEnd="?android:listPreferredItemPaddingEnd">
android:layout_marginHorizontal="@dimen/margin_normal"
android:layout_marginTop="@dimen/margin_small"
android:text="@string/speed"
android:textAppearance="?attr/textAppearanceBodySmall"
android:visibility="gone"
tools:visibility="visible" />
<TextView
android:id="@+id/textView_timer"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:drawablePadding="?android:listPreferredItemPaddingStart"
android:ellipsize="end"
android:singleLine="true"
android:text="@string/automatic_scroll"
android:textAppearance="?attr/textAppearanceButton"
android:textColor="@color/list_item_text_color"
app:drawableStartCompat="@drawable/ic_timer"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/slider_timer"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_default="wrap" />
<com.google.android.material.slider.Slider
android:id="@+id/slider_timer"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_normal"
android:contentDescription="@string/automatic_scroll"
android:labelFor="@id/textView_timer"
android:valueFrom="0"
android:valueTo="1"
app:labelBehavior="floating"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/textView_timer"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_min="120dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.slider.Slider
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="1"
android:visibility="gone"
app:labelBehavior="gone"
tools:visibility="visible" />
<org.koitharu.kotatsu.base.ui.widgets.ListItemTextView
android:id="@+id/button_color_filter"

View File

@@ -430,4 +430,5 @@
<string name="settings_apply_restart_required">Please restart the application to apply these changes</string>
<string name="comics_archive_import_description">You can select one or more .cbz or .zip files, each file will be recognized as a separate manga.</string>
<string name="folder_with_images_import_description">You can select a directory with archives or images. Each archive (or subdirectory) will be recognized as a chapter.</string>
<string name="speed">Speed</string>
</resources>