Auto scroll update (#7)

* updated auto-scroll logic for EPUB and PDF

* Refactored auto-scroll to use temporary debounced pauses instead of hard stops on manual interaction.
* Updated tap handling to allow toggling bars without interrupting scroll.
* Added a 2-second orientation pause on EPUB chapter transitions.
* Enhanced AutoScrollControls UI with circular progress overlay to indicate the "temporarily paused" state.

* Refactored auto-scroll controls

This commit refactors the auto-scroll controls for both EPUB and PDF readers, introducing a more advanced and user-friendly interface.

Key changes:
- Redesigned the auto-scroll controls UI into a collapsible and lockable card.
- Added a "lock" feature to hide controls during scrolling.
- Introduced an alternative slider-based input for speed adjustment, which can be toggled.
- Persisted the lock and input mode states in SharedPreferences.
- Improved the EPUB auto-scroll mechanism using `translate3d` for smoother sub-pixel scrolling.
This commit is contained in:
Aryan 2026-02-26 17:16:01 +05:30 committed by GitHub
parent 1f111034f1
commit de376e2ace
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 410 additions and 238 deletions

View file

@ -25,8 +25,8 @@ android {
applicationId = "com.aryan.reader"
minSdk = 26
targetSdk = 35
versionCode = 32
versionName = "1.0.31"
versionCode = 33
versionName = "1.0.32"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
externalNativeBuild {

View file

@ -30,6 +30,12 @@
word-break: break-word; -webkit-text-size-adjust: 100%;
text-rendering: optimizeLegibility; -webkit-user-select: none;
-moz-user-select: none; -ms-user-select: none; user-select: none;
scroll-behavior: auto !important;
}
#content-container {
will-change: transform;
transition: none !important;
}
p, div, li, td, th, span {
@ -2649,7 +2655,7 @@ window.findFirstVisibleCfi=function (cfiArray) {
})();
(function () {
const TAG_AUTO_SCROLL="AutoScrollDiagnosis"; // Define the tag
const TAG_AUTO_SCROLL="AutoScrollDiagnosis";
window.autoScroll= {
active: false,
@ -2672,27 +2678,21 @@ window.findFirstVisibleCfi=function (cfiArray) {
this.accumulator=0.0;
if (this.animationId) cancelAnimationFrame(this.animationId);
this.loop();
}
,
},
stop: function () {
console.log(`$ {
TAG_AUTO_SCROLL
}
: Stopping auto-scroll.`);
this.active = false;
const container=document.getElementById('content-container') || document.body;
if (container) container.style.transform='none';
if (this.animationId) {
cancelAnimationFrame(this.animationId);
this.animationId = null;
}
}
,
const container = document.getElementById('content-container') || document.body;
if (container) {
container.style.transform = 'none';
window.scrollBy(0, 0);
}
},
updateSpeed: function (newSpeed) {
console.log(`$ {
@ -2714,105 +2714,35 @@ window.findFirstVisibleCfi=function (cfiArray) {
this.accumulator += this.speed;
const pixelsToScroll=Math.floor(this.accumulator);
const subPixelRemainder=this.accumulator - pixelsToScroll;
const totalPixelsToScroll = Math.floor(this.accumulator);
if (pixelsToScroll >=1) {
if (totalPixelsToScroll >= 1) {
const prevScrollY = window.scrollY;
window.scrollBy(0, pixelsToScroll);
this.accumulator -=pixelsToScroll;
window.scrollBy(0, totalPixelsToScroll);
this.accumulator -= totalPixelsToScroll;
// Calculation logic
const innerH=window.innerHeight;
const scrollY = window.scrollY;
const docHeight=document.documentElement.scrollHeight; // Usually more reliable than body.scrollHeight
const currentScrollPos=scrollY + innerH;
// Relaxed threshold slightly (from 2 to 3) to account for sub-pixel rendering differences
const isAtBottom=currentScrollPos >=(docHeight - 3);
const isStuck=(scrollY===prevScrollY);
// Log only when we are very close to the bottom or stuck, to avoid spamming
if (isAtBottom || isStuck || (docHeight - currentScrollPos < 50)) {
console.log(`$ {
TAG_AUTO_SCROLL
}
: Loop Status -> ScrollY: $ {
Math.round(scrollY)
}
, InnerH: $ {
innerH
}
, Pos: $ {
Math.round(currentScrollPos)
}
, DocH: $ {
docHeight
}
`);
console.log(`$ {
TAG_AUTO_SCROLL
}
: Check -> AtBottom: $ {
isAtBottom
}
, Stuck: $ {
isStuck
}
`);
}
const docHeight = document.documentElement.scrollHeight;
const innerH = window.innerHeight;
const isAtBottom = (scrollY + innerH) >= (docHeight - 3);
const isStuck = (totalPixelsToScroll > 0 && scrollY === prevScrollY && prevScrollY > 0);
if (isAtBottom || isStuck) {
console.log(`$ {
TAG_AUTO_SCROLL
}
: End of chapter detected. Calling Bridge.`);
this.stop();
const container=document.getElementById('content-container') || document.body;
container.style.transform='none';
if (window.AutoScrollBridge && window.AutoScrollBridge.onChapterEnd) {
window.AutoScrollBridge.onChapterEnd();
}
else {
console.log(`$ {
TAG_AUTO_SCROLL
}
: AutoScrollBridge not found !`);
}
return;
}
}
const container = document.getElementById('content-container') || document.body;
if (container) {
container.style.transform=`translateY(-$ {
subPixelRemainder
}
px)`;
container.style.transform = `translate3d(0, -${this.accumulator}px, 0)`;
}
this.animationId = requestAnimationFrame(this.loop.bind(this));
}
,
}
;
},
};
})();

View file

@ -17,6 +17,7 @@
*
* mail: epistemereader@gmail.com
*/
// EpubReaderControls.kt
package com.aryan.reader.epubreader
import android.annotation.SuppressLint
@ -71,12 +72,16 @@ import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.ChevronLeft
import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.LockOpen
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Remove
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.SwapHoriz
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
@ -655,6 +660,7 @@ suspend fun captureWebViewVisibleArea(webView: WebView): Bitmap? {
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AutoScrollControls(
isPlaying: Boolean,
@ -664,15 +670,23 @@ fun AutoScrollControls(
onClose: () -> Unit,
isCollapsed: Boolean,
onCollapseChange: (Boolean) -> Unit,
modifier: Modifier = Modifier
isLocked: Boolean,
onLockToggle: () -> Unit,
useSlider: Boolean,
onInputModeToggle: () -> Unit,
modifier: Modifier = Modifier,
maxSpeed: Float = 10f,
isTempPaused: Boolean = false,
) {
Surface(
shape = RoundedCornerShape(50),
shape = RoundedCornerShape(28.dp),
color = MaterialTheme.colorScheme.surfaceContainerHigh,
tonalElevation = 6.dp,
tonalElevation = 8.dp,
shadowElevation = 6.dp,
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant),
modifier = modifier.animateContentSize()
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)),
modifier = modifier
.widthIn(max = 400.dp)
.animateContentSize()
) {
AnimatedContent(
targetState = isCollapsed,
@ -681,26 +695,31 @@ fun AutoScrollControls(
},
label = "AutoScrollUnified"
) { collapsed ->
Row(
modifier = Modifier.padding(6.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
if (collapsed) {
// COLLAPSED STATE: Ultra-Compact Pill
Row(
modifier = Modifier
.padding(horizontal = 6.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
// Expand Button
IconButton(
onClick = { onCollapseChange(false) },
modifier = Modifier.size(40.dp)
modifier = Modifier.size(36.dp)
) {
Icon(
imageVector = Icons.Default.ChevronLeft,
contentDescription = "Expand",
tint = MaterialTheme.colorScheme.onSurface
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
// Play/Pause Mini
Box(modifier = Modifier.size(36.dp), contentAlignment = Alignment.Center) {
FilledIconButton(
onClick = onPlayPauseToggle,
modifier = Modifier.size(40.dp),
modifier = Modifier.size(36.dp),
colors = IconButtonDefaults.filledIconButtonColors(
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary
@ -709,67 +728,102 @@ fun AutoScrollControls(
Icon(
imageVector = if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow,
contentDescription = if (isPlaying) "Pause" else "Play",
modifier = Modifier.size(24.dp)
modifier = Modifier.size(20.dp)
)
}
if (isTempPaused) {
CircularProgressIndicator(
modifier = Modifier.size(36.dp),
color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.5f),
strokeWidth = 2.dp
)
}
}
}
} else {
// EXPANDED STATE: Card-like Layout
Column(
modifier = Modifier.padding(16.dp)
) {
// Top Row: Utility Actions (Right aligned)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
// Label
Text(
text = "Auto Scroll",
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary
)
// Tools Row
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
// Lock
IconButton(
onClick = onLockToggle,
modifier = Modifier.size(32.dp)
) {
Icon(
imageVector = if (isLocked) Icons.Default.LockOpen else Icons.Default.Lock,
contentDescription = if (isLocked) "Unlock" else "Lock",
modifier = Modifier.size(18.dp),
tint = if (isLocked) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
)
}
// Swap Input
IconButton(
onClick = onInputModeToggle,
modifier = Modifier.size(32.dp)
) {
Icon(
imageVector = Icons.Default.SwapHoriz,
contentDescription = "Swap Controls",
modifier = Modifier.size(18.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
// Collapse
IconButton(
onClick = { onCollapseChange(true) },
modifier = Modifier.size(32.dp)
) {
Icon(
imageVector = Icons.Default.ChevronRight,
contentDescription = "Collapse",
modifier = Modifier.size(18.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
// Close
IconButton(
onClick = onClose,
modifier = Modifier.size(40.dp)
modifier = Modifier.size(32.dp)
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = "Close",
tint = MaterialTheme.colorScheme.error,
modifier = Modifier.size(20.dp)
modifier = Modifier.size(18.dp)
)
}
}
}
Box(
modifier = Modifier
.width(1.dp)
.height(24.dp)
.background(MaterialTheme.colorScheme.outlineVariant)
)
Spacer(Modifier.height(16.dp))
// Bottom Row: Primary Controls
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(0.dp)
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
IconButton(
onClick = { onSpeedChange((speed - 0.1f).coerceAtLeast(0.1f)) },
modifier = Modifier.size(36.dp)
) {
Icon(Icons.Default.Remove, "Slower", modifier = Modifier.size(18.dp))
}
Text(
text = "%.1fx".format(speed),
style = MaterialTheme.typography.labelLarge.copy(fontFeatureSettings = "tnum"),
modifier = Modifier.widthIn(min = 40.dp),
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurface
)
IconButton(
onClick = { onSpeedChange((speed + 0.1f).coerceAtMost(10f)) },
modifier = Modifier.size(36.dp)
) {
Icon(Icons.Default.Add, "Faster", modifier = Modifier.size(18.dp))
}
}
Box(
modifier = Modifier
.width(1.dp)
.height(24.dp)
.background(MaterialTheme.colorScheme.outlineVariant)
)
// 1. Play/Pause Button
Box(modifier = Modifier.size(48.dp), contentAlignment = Alignment.Center) {
FilledIconButton(
onClick = onPlayPauseToggle,
modifier = Modifier.size(40.dp),
modifier = Modifier.size(48.dp),
colors = IconButtonDefaults.filledIconButtonColors(
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary
@ -781,18 +835,88 @@ fun AutoScrollControls(
modifier = Modifier.size(24.dp)
)
}
IconButton(
onClick = { onCollapseChange(true) },
modifier = Modifier.size(40.dp)
) {
Icon(
imageVector = Icons.Default.ChevronRight,
contentDescription = "Collapse",
tint = MaterialTheme.colorScheme.onSurfaceVariant
if (isTempPaused) {
CircularProgressIndicator(
modifier = Modifier.size(48.dp),
color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.5f),
strokeWidth = 3.dp
)
}
}
// 2. Speed Controls (Weight to fill space)
Box(
modifier = Modifier.weight(1f),
contentAlignment = Alignment.Center
) {
if (useSlider) {
// Slider Mode: Text + Slider
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
text = "%.1fx".format(speed),
style = MaterialTheme.typography.titleMedium.copy(fontFeatureSettings = "tnum"),
modifier = Modifier.width(45.dp),
textAlign = TextAlign.End
)
val minSpeed = 0.1f
val steps = ((maxSpeed - minSpeed) / 0.1f).roundToInt() - 1
Slider(
value = speed,
onValueChange = { onSpeedChange((it * 10f).roundToInt() / 10f) },
valueRange = minSpeed..maxSpeed,
steps = if (steps > 0) steps else 0,
modifier = Modifier.weight(1f),
thumb = {
Surface(
modifier = Modifier.size(20.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primary,
shadowElevation = 2.dp
) {}
}
)
}
} else {
// Stepper Mode: Segmented Pill
Surface(
shape = RoundedCornerShape(50),
color = MaterialTheme.colorScheme.surfaceVariant,
modifier = Modifier.height(48.dp).fillMaxWidth()
) {
Row(
modifier = Modifier.fillMaxSize(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
IconButton(
onClick = { onSpeedChange((speed - 0.1f).coerceAtLeast(0.1f)) },
modifier = Modifier.size(48.dp)
) {
Icon(Icons.Default.Remove, "Slower")
}
Text(
text = "%.1fx".format(speed),
style = MaterialTheme.typography.titleMedium.copy(fontFeatureSettings = "tnum"),
textAlign = TextAlign.Center
)
IconButton(
onClick = { onSpeedChange((speed + 0.1f).coerceAtMost(10f)) },
modifier = Modifier.size(48.dp)
) {
Icon(Icons.Default.Add, "Faster")
}
}
}
}
}
}
}
}
}
}

View file

@ -17,6 +17,7 @@
*
* mail: epistemereader@gmail.com
*/
// EpubReaderScreen.kt
@file:OptIn(ExperimentalSerializationApi::class) @file:Suppress("VariableNeverRead")
package com.aryan.reader.epubreader
@ -171,6 +172,30 @@ import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
import androidx.compose.ui.BiasAlignment
import androidx.core.content.edit
private const val AUTO_SCROLL_LOCKED_KEY = "auto_scroll_locked"
private const val AUTO_SCROLL_USE_SLIDER_KEY = "auto_scroll_use_slider"
private fun saveAutoScrollLocked(context: Context, isLocked: Boolean) {
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
prefs.edit { putBoolean(AUTO_SCROLL_LOCKED_KEY, isLocked)}
}
private fun loadAutoScrollLocked(context: Context): Boolean {
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
return prefs.getBoolean(AUTO_SCROLL_LOCKED_KEY, false)
}
private fun saveAutoScrollUseSlider(context: Context, useSlider: Boolean) {
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
prefs.edit { putBoolean(AUTO_SCROLL_USE_SLIDER_KEY, useSlider) }
}
private fun loadAutoScrollUseSlider(context: Context): Boolean {
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
return prefs.getBoolean(AUTO_SCROLL_USE_SLIDER_KEY, false)
}
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@Composable
@ -607,9 +632,14 @@ fun EpubReaderHost(
var isAutoScrollModeActive by remember { mutableStateOf(false) }
var isAutoScrollPlaying by remember { mutableStateOf(false) }
var isAutoScrollTempPaused by remember { mutableStateOf(false) }
val autoScrollResumeJob = remember { mutableStateOf<Job?>(null) }
var autoScrollSpeed by remember { mutableFloatStateOf(loadAutoScrollSpeed(context)) }
var isAutoScrollCollapsed by remember { mutableStateOf(false) }
var isAutoScrollLocked by remember { mutableStateOf(loadAutoScrollLocked(context)) }
var autoScrollUseSlider by remember { mutableStateOf(loadAutoScrollUseSlider(context)) }
DisposableEffect(Unit) {
onDispose {
Timber.d("Disposing sample MediaPlayer.")
@ -618,10 +648,28 @@ fun EpubReaderHost(
}
fun updateAutoScrollState(playing: Boolean, speed: Float) {
updateAutoScrollJs(webViewRefForTts, playing, speed)
val effectivePlaying = playing && !isAutoScrollTempPaused
updateAutoScrollJs(webViewRefForTts, effectivePlaying, speed)
}
LaunchedEffect(isAutoScrollModeActive, isAutoScrollPlaying, autoScrollSpeed) {
fun triggerAutoScrollTempPause(durationMs: Long) {
if (!isAutoScrollModeActive || !isAutoScrollPlaying) return
autoScrollResumeJob.value?.cancel()
isAutoScrollTempPaused = true
updateAutoScrollState(isAutoScrollPlaying, autoScrollSpeed)
autoScrollResumeJob.value = scope.launch {
delay(durationMs)
if (isActive && isAutoScrollModeActive && isAutoScrollPlaying) {
isAutoScrollTempPaused = false
@Suppress("KotlinConstantConditions") updateAutoScrollState(isAutoScrollPlaying, autoScrollSpeed)
}
}
}
LaunchedEffect(isAutoScrollModeActive, isAutoScrollPlaying, autoScrollSpeed, isAutoScrollTempPaused) {
if (isAutoScrollModeActive) {
updateAutoScrollState(isAutoScrollPlaying, autoScrollSpeed)
} else {
@ -629,12 +677,6 @@ fun EpubReaderHost(
}
}
fun pauseAutoScroll() {
if (isAutoScrollModeActive && isAutoScrollPlaying) {
isAutoScrollPlaying = false
}
}
fun startTts() {
if (isAutoScrollModeActive) {
isAutoScrollModeActive = false
@ -1615,11 +1657,8 @@ fun EpubReaderHost(
}
if (isAutoScrollModeActive && isAutoScrollPlaying) {
Timber.d("Continuing Auto-Scroll for new chapter.")
scope.launch {
delay(300)
updateAutoScrollState(true, autoScrollSpeed)
}
Timber.d("Continuing Auto-Scroll for new chapter with delay.")
triggerAutoScrollTempPause(1000L)
}
},
onTap = {
@ -1637,7 +1676,6 @@ fun EpubReaderHost(
Timber.d("Chapter tapped, showing main bars.")
}
}
pauseAutoScroll()
},
onPotentialScroll = {
if (showBars) {
@ -1645,7 +1683,9 @@ fun EpubReaderHost(
showFormatAdjustmentBars = false
Timber.d("Scroll/Drag detected, hiding bars.")
}
pauseAutoScroll()
if (isAutoScrollModeActive && isAutoScrollPlaying) {
triggerAutoScrollTempPause(1000L)
}
},
onAutoScrollChapterEnd = {
Timber.d("Screen: onAutoScrollChapterEnd triggered. Current Index: $currentChapterIndex")
@ -2620,6 +2660,7 @@ fun EpubReaderHost(
isAutoScrollModeActive = true
isAutoScrollPlaying = true
showBars = false
showBars = true
},
searchFocusRequester = searchFocusRequester,
modifier = Modifier.align(Alignment.TopCenter)
@ -2635,8 +2676,10 @@ fun EpubReaderHost(
label = "AutoScrollAlignAnimation"
)
val isAutoScrollControlsVisible = isAutoScrollModeActive && (!isAutoScrollLocked || showBars)
AnimatedVisibility(
visible = isAutoScrollModeActive,
visible = isAutoScrollControlsVisible,
enter = slideInVertically { it } + fadeIn(),
exit = slideOutVertically { it } + fadeOut(),
modifier = Modifier
@ -2646,8 +2689,19 @@ fun EpubReaderHost(
) {
AutoScrollControls(
isPlaying = isAutoScrollPlaying,
onPlayPauseToggle = { isAutoScrollPlaying = !isAutoScrollPlaying },
isTempPaused = isAutoScrollTempPaused,
onPlayPauseToggle = {
if (isAutoScrollPlaying) {
isAutoScrollPlaying = false
isAutoScrollTempPaused = false
autoScrollResumeJob.value?.cancel()
} else {
isAutoScrollPlaying = true
isAutoScrollTempPaused = false
}
},
speed = autoScrollSpeed,
maxSpeed = 10f,
onSpeedChange = {
autoScrollSpeed = it
saveAutoScrollSpeed(context, it)
@ -2658,7 +2712,17 @@ fun EpubReaderHost(
showBars = true
},
isCollapsed = isAutoScrollCollapsed,
onCollapseChange = { isAutoScrollCollapsed = it }
onCollapseChange = { isAutoScrollCollapsed = it },
isLocked = isAutoScrollLocked,
onLockToggle = {
isAutoScrollLocked = !isAutoScrollLocked
saveAutoScrollLocked(context, isAutoScrollLocked)
},
useSlider = autoScrollUseSlider,
onInputModeToggle = {
autoScrollUseSlider = !autoScrollUseSlider
saveAutoScrollUseSlider(context, autoScrollUseSlider)
}
)
}

View file

@ -213,6 +213,7 @@ internal fun PdfVerticalReader(
topContentPaddingPx: Float = 0f,
onTextBoxMoved: (String, Int, Rect) -> Unit = { _, _, _ -> },
isAutoScrollPlaying: Boolean = false,
isAutoScrollTempPaused: Boolean = false,
autoScrollSpeed: Float = 1.0f,
onInteractionListener: () -> Unit = {}
) {
@ -484,9 +485,9 @@ internal fun PdfVerticalReader(
}
}
LaunchedEffect(isAutoScrollPlaying, autoScrollSpeed, totalDocHeight, screenHeight) {
if (isAutoScrollPlaying) {
val baseSpeedPxPerSec = 30f
LaunchedEffect(isAutoScrollPlaying, isAutoScrollTempPaused, autoScrollSpeed, totalDocHeight, screenHeight) {
if (isAutoScrollPlaying && !isAutoScrollTempPaused) {
val baseSpeedPxPerSec = 80f
var lastFrameTime = withFrameNanos { it }
while (isActive) {

View file

@ -284,6 +284,28 @@ private const val DOCK_LOCATION_KEY = "dock_location"
private const val DOCK_OFFSET_X_KEY = "dock_offset_x"
private const val DOCK_OFFSET_Y_KEY = "dock_offset_y"
private const val PDF_AUTO_SCROLL_SPEED_KEY = "pdf_auto_scroll_speed"
private const val PDF_AUTO_SCROLL_LOCKED_KEY = "pdf_auto_scroll_locked"
private const val PDF_AUTO_SCROLL_USE_SLIDER_KEY = "pdf_auto_scroll_use_slider"
private fun savePdfAutoScrollLocked(context: Context, isLocked: Boolean) {
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
prefs.edit { putBoolean(PDF_AUTO_SCROLL_LOCKED_KEY, isLocked) }
}
private fun loadPdfAutoScrollLocked(context: Context): Boolean {
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
return prefs.getBoolean(PDF_AUTO_SCROLL_LOCKED_KEY, false)
}
private fun savePdfAutoScrollUseSlider(context: Context, useSlider: Boolean) {
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
prefs.edit { putBoolean(PDF_AUTO_SCROLL_USE_SLIDER_KEY, useSlider) }
}
private fun loadPdfAutoScrollUseSlider(context: Context): Boolean {
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
return prefs.getBoolean(PDF_AUTO_SCROLL_USE_SLIDER_KEY, false)
}
private fun savePdfAutoScrollSpeed(context: Context, speed: Float) {
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
@ -604,13 +626,30 @@ fun PdfViewerScreen(
var isAutoScrollModeActive by remember { mutableStateOf(false) }
var isAutoScrollPlaying by remember { mutableStateOf(false) }
var isAutoScrollTempPaused by remember { mutableStateOf(false) }
val autoScrollResumeJob = remember { mutableStateOf<Job?>(null) }
var autoScrollSpeed by remember { mutableFloatStateOf(loadPdfAutoScrollSpeed(context)) }
var isAutoScrollCollapsed by remember { mutableStateOf(false) }
var isAutoScrollLocked by remember { mutableStateOf(loadPdfAutoScrollLocked(context)) }
var autoScrollUseSlider by remember { mutableStateOf(loadPdfAutoScrollUseSlider(context)) }
fun triggerAutoScrollTempPause(durationMs: Long) {
if (!isAutoScrollModeActive || !isAutoScrollPlaying) return
autoScrollResumeJob.value?.cancel()
isAutoScrollTempPaused = true
autoScrollResumeJob.value = coroutineScope.launch {
delay(durationMs)
if (isActive && isAutoScrollModeActive && isAutoScrollPlaying) {
isAutoScrollTempPaused = false
}
}
}
val onAutoScrollInteraction = remember {
{
if (isAutoScrollPlaying) {
isAutoScrollPlaying = false
triggerAutoScrollTempPause(1000L)
}
}
}
@ -1102,7 +1141,6 @@ fun PdfViewerScreen(
val onSingleTapStable = remember {
{
onAutoScrollInteraction()
if (selectedTextBoxId != null) {
val box = textBoxes.find { it.id == selectedTextBoxId }
if (box != null && box.text.trim().isEmpty()) {
@ -3641,6 +3679,7 @@ fun PdfViewerScreen(
}
},
isAutoScrollPlaying = isAutoScrollPlaying,
isAutoScrollTempPaused = isAutoScrollTempPaused,
autoScrollSpeed = autoScrollSpeed,
onInteractionListener = onAutoScrollInteraction
)
@ -4116,7 +4155,7 @@ fun PdfViewerScreen(
showMoreMenu = false
isAutoScrollModeActive = true
isAutoScrollPlaying = true
showBars = false
showBars = true
}
)
HorizontalDivider()
@ -5554,13 +5593,15 @@ fun PdfViewerScreen(
label = "AutoScrollPadding"
)
val isAutoScrollControlsVisible = isAutoScrollModeActive && (!isAutoScrollLocked || showBars)
val alignmentBias by animateFloatAsState(
targetValue = if (isAutoScrollCollapsed) 1f else 0f,
label = "AutoScrollAlignAnimation"
)
AnimatedVisibility(
visible = isAutoScrollModeActive,
visible = isAutoScrollControlsVisible,
enter = slideInVertically { it } + fadeIn(),
exit = slideOutVertically { it } + fadeOut(),
modifier = Modifier
@ -5570,8 +5611,10 @@ fun PdfViewerScreen(
) {
AutoScrollControls(
isPlaying = isAutoScrollPlaying,
isTempPaused = isAutoScrollTempPaused,
onPlayPauseToggle = { isAutoScrollPlaying = !isAutoScrollPlaying },
speed = autoScrollSpeed,
maxSpeed = 20f,
onSpeedChange = {
autoScrollSpeed = it
savePdfAutoScrollSpeed(context, it)
@ -5582,7 +5625,17 @@ fun PdfViewerScreen(
showBars = true
},
isCollapsed = isAutoScrollCollapsed,
onCollapseChange = { isAutoScrollCollapsed = it }
onCollapseChange = { isAutoScrollCollapsed = it },
isLocked = isAutoScrollLocked,
onLockToggle = {
isAutoScrollLocked = !isAutoScrollLocked
savePdfAutoScrollLocked(context, isAutoScrollLocked)
},
useSlider = autoScrollUseSlider,
onInputModeToggle = {
autoScrollUseSlider = !autoScrollUseSlider
savePdfAutoScrollUseSlider(context, autoScrollUseSlider)
}
)
}
}