From 43d55cedaec60e679550e314ef881b8e410e9559 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 24 Mar 2023 15:03:40 +0200 Subject: [PATCH] Refactor scroll timer --- .../kotatsu/core/prefs/AppSettings.kt | 7 ++ .../kotatsu/reader/ui/ReaderActivity.kt | 22 +++-- .../reader/ui/ReaderControlDelegate.kt | 2 + .../koitharu/kotatsu/reader/ui/ScrollTimer.kt | 94 +++++++++++++------ .../reader/ui/config/PageSwitchTimer.kt | 75 --------------- .../ui/config/ReaderConfigBottomSheet.kt | 44 +++++++-- .../main/res/layout/sheet_reader_config.xml | 75 +++++++-------- app/src/main/res/values/strings.xml | 1 + 8 files changed, 162 insertions(+), 158 deletions(-) delete mode 100644 app/src/main/java/org/koitharu/kotatsu/reader/ui/config/PageSwitchTimer.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt index 3faf6e5b7..6386dff7c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -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" diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt index 24d09eb3c..b324d9b87 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt @@ -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,14 +70,17 @@ class ReaderActivity : private val viewModel: ReaderViewModel by viewModels() - override var autoScrollSpeed: Float - get() = scrollTimer.speed + override val readerMode: ReaderMode? + get() = readerManager.currentMode + + override var isAutoScrollEnabled: Boolean + get() = scrollTimer.isEnabled set(value) { - scrollTimer.speed = value + scrollTimer.isEnabled = value } - override val readerMode: ReaderMode? - get() = readerManager.currentMode + @Inject + lateinit var scrollTimerFactory: ScrollTimer.Factory private lateinit var scrollTimer: ScrollTimer private lateinit var touchHelper: GridTouchHelper @@ -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 } diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderControlDelegate.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderControlDelegate.kt index 1e87ce21c..3791ac5c4 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderControlDelegate.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderControlDelegate.kt @@ -142,5 +142,7 @@ class ReaderControlDelegate( fun scrollBy(delta: Int): Boolean fun toggleUiVisibility() + + fun isReaderResumed(): Boolean } } diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ScrollTimer.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ScrollTimer.kt index 894b33921..da3129bf4 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ScrollTimer.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ScrollTimer.kt @@ -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 + } } diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/config/PageSwitchTimer.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/config/PageSwitchTimer.kt deleted file mode 100644 index ac4134a25..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/config/PageSwitchTimer.kt +++ /dev/null @@ -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 - } - } -} diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/config/ReaderConfigBottomSheet.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/config/ReaderConfigBottomSheet.kt index 10bf4f6be..d0a1af3d5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/config/ReaderConfigBottomSheet.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/config/ReaderConfigBottomSheet.kt @@ -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(), ActivityResultCallback, View.OnClickListener, MaterialButtonToggleGroup.OnButtonCheckedListener, - Slider.OnChangeListener { + Slider.OnChangeListener, CompoundButton.OnCheckedChangeListener { private val viewModel by activityViewModels() 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) } diff --git a/app/src/main/res/layout/sheet_reader_config.xml b/app/src/main/res/layout/sheet_reader_config.xml index 4521d89b9..5dbddcee6 100644 --- a/app/src/main/res/layout/sheet_reader_config.xml +++ b/app/src/main/res/layout/sheet_reader_config.xml @@ -103,51 +103,44 @@ android:text="@string/reader_mode_hint" android:textAppearance="?attr/textAppearanceBodySmall" /> - + android:paddingEnd="?android:listPreferredItemPaddingEnd" + android:singleLine="true" + android:text="@string/automatic_scroll" + android:textAppearance="?attr/textAppearanceButton" + android:textColor="?colorOnSurfaceVariant" + app:drawableStartCompat="@drawable/ic_timer" /> - - - - - + + + Please restart the application to apply these changes You can select one or more .cbz or .zip files, each file will be recognized as a separate manga. You can select a directory with archives or images. Each archive (or subdirectory) will be recognized as a chapter. + Speed