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" applicationId = "com.aryan.reader"
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 32 versionCode = 33
versionName = "1.0.31" versionName = "1.0.32"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
externalNativeBuild { externalNativeBuild {

View file

@ -30,6 +30,12 @@
word-break: break-word; -webkit-text-size-adjust: 100%; word-break: break-word; -webkit-text-size-adjust: 100%;
text-rendering: optimizeLegibility; -webkit-user-select: none; text-rendering: optimizeLegibility; -webkit-user-select: none;
-moz-user-select: none; -ms-user-select: none; 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 { p, div, li, td, th, span {
@ -2649,7 +2655,7 @@ window.findFirstVisibleCfi=function (cfiArray) {
})(); })();
(function () { (function () {
const TAG_AUTO_SCROLL="AutoScrollDiagnosis"; // Define the tag const TAG_AUTO_SCROLL="AutoScrollDiagnosis";
window.autoScroll= { window.autoScroll= {
active: false, active: false,
@ -2672,27 +2678,21 @@ window.findFirstVisibleCfi=function (cfiArray) {
this.accumulator=0.0; this.accumulator=0.0;
if (this.animationId) cancelAnimationFrame(this.animationId); if (this.animationId) cancelAnimationFrame(this.animationId);
this.loop(); this.loop();
} },
,
stop: function () { stop: function () {
console.log(`$ { this.active = false;
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) { if (this.animationId) {
cancelAnimationFrame(this.animationId); cancelAnimationFrame(this.animationId);
this.animationId=null; this.animationId = null;
}
} }
, const container = document.getElementById('content-container') || document.body;
if (container) {
container.style.transform = 'none';
window.scrollBy(0, 0);
}
},
updateSpeed: function (newSpeed) { updateSpeed: function (newSpeed) {
console.log(`$ { console.log(`$ {
@ -2710,109 +2710,39 @@ window.findFirstVisibleCfi=function (cfiArray) {
, ,
loop: function () { loop: function () {
if ( !this.active) return; if (!this.active) return;
this.accumulator +=this.speed; this.accumulator += this.speed;
const pixelsToScroll=Math.floor(this.accumulator); const totalPixelsToScroll = Math.floor(this.accumulator);
const subPixelRemainder=this.accumulator - pixelsToScroll;
if (pixelsToScroll >=1) { if (totalPixelsToScroll >= 1) {
const prevScrollY=window.scrollY; const prevScrollY = window.scrollY;
window.scrollBy(0, pixelsToScroll); window.scrollBy(0, totalPixelsToScroll);
this.accumulator -=pixelsToScroll;
// Calculation logic this.accumulator -= totalPixelsToScroll;
const innerH=window.innerHeight;
const scrollY=window.scrollY;
const docHeight=document.documentElement.scrollHeight; // Usually more reliable than body.scrollHeight
const currentScrollPos=scrollY + innerH; const scrollY = window.scrollY;
const docHeight = document.documentElement.scrollHeight;
// Relaxed threshold slightly (from 2 to 3) to account for sub-pixel rendering differences const innerH = window.innerHeight;
const isAtBottom=currentScrollPos >=(docHeight - 3); const isAtBottom = (scrollY + innerH) >= (docHeight - 3);
const isStuck=(scrollY===prevScrollY); const isStuck = (totalPixelsToScroll > 0 && scrollY === prevScrollY && prevScrollY > 0);
// 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
}
`);
}
if (isAtBottom || isStuck) { if (isAtBottom || isStuck) {
console.log(`$ {
TAG_AUTO_SCROLL
}
: End of chapter detected. Calling Bridge.`);
this.stop(); this.stop();
const container=document.getElementById('content-container') || document.body;
container.style.transform='none';
if (window.AutoScrollBridge && window.AutoScrollBridge.onChapterEnd) { if (window.AutoScrollBridge && window.AutoScrollBridge.onChapterEnd) {
window.AutoScrollBridge.onChapterEnd(); window.AutoScrollBridge.onChapterEnd();
} }
else {
console.log(`$ {
TAG_AUTO_SCROLL
}
: AutoScrollBridge not found !`);
}
return; return;
} }
} }
const container=document.getElementById('content-container') || document.body; const container = document.getElementById('content-container') || document.body;
if (container) { if (container) {
container.style.transform=`translateY(-$ { container.style.transform = `translate3d(0, -${this.accumulator}px, 0)`;
subPixelRemainder
} }
px)`; this.animationId = requestAnimationFrame(this.loop.bind(this));
} },
};
this.animationId=requestAnimationFrame(this.loop.bind(this));
}
,
}
;
})(); })();

View file

@ -17,6 +17,7 @@
* *
* mail: epistemereader@gmail.com * mail: epistemereader@gmail.com
*/ */
// EpubReaderControls.kt
package com.aryan.reader.epubreader package com.aryan.reader.epubreader
import android.annotation.SuppressLint 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.ChevronLeft
import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material.icons.filled.Close 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.Menu
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Pause import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Remove import androidx.compose.material.icons.filled.Remove
import androidx.compose.material.icons.filled.Search 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.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@ -655,6 +660,7 @@ suspend fun captureWebViewVisibleArea(webView: WebView): Bitmap? {
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AutoScrollControls( fun AutoScrollControls(
isPlaying: Boolean, isPlaying: Boolean,
@ -664,15 +670,23 @@ fun AutoScrollControls(
onClose: () -> Unit, onClose: () -> Unit,
isCollapsed: Boolean, isCollapsed: Boolean,
onCollapseChange: (Boolean) -> Unit, onCollapseChange: (Boolean) -> Unit,
modifier: Modifier = Modifier isLocked: Boolean,
onLockToggle: () -> Unit,
useSlider: Boolean,
onInputModeToggle: () -> Unit,
modifier: Modifier = Modifier,
maxSpeed: Float = 10f,
isTempPaused: Boolean = false,
) { ) {
Surface( Surface(
shape = RoundedCornerShape(50), shape = RoundedCornerShape(28.dp),
color = MaterialTheme.colorScheme.surfaceContainerHigh, color = MaterialTheme.colorScheme.surfaceContainerHigh,
tonalElevation = 6.dp, tonalElevation = 8.dp,
shadowElevation = 6.dp, shadowElevation = 6.dp,
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)),
modifier = modifier.animateContentSize() modifier = modifier
.widthIn(max = 400.dp)
.animateContentSize()
) { ) {
AnimatedContent( AnimatedContent(
targetState = isCollapsed, targetState = isCollapsed,
@ -681,26 +695,31 @@ fun AutoScrollControls(
}, },
label = "AutoScrollUnified" label = "AutoScrollUnified"
) { collapsed -> ) { collapsed ->
Row(
modifier = Modifier.padding(6.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
if (collapsed) { 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( IconButton(
onClick = { onCollapseChange(false) }, onClick = { onCollapseChange(false) },
modifier = Modifier.size(40.dp) modifier = Modifier.size(36.dp)
) { ) {
Icon( Icon(
imageVector = Icons.Default.ChevronLeft, imageVector = Icons.Default.ChevronLeft,
contentDescription = "Expand", contentDescription = "Expand",
tint = MaterialTheme.colorScheme.onSurface tint = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
// Play/Pause Mini
Box(modifier = Modifier.size(36.dp), contentAlignment = Alignment.Center) {
FilledIconButton( FilledIconButton(
onClick = onPlayPauseToggle, onClick = onPlayPauseToggle,
modifier = Modifier.size(40.dp), modifier = Modifier.size(36.dp),
colors = IconButtonDefaults.filledIconButtonColors( colors = IconButtonDefaults.filledIconButtonColors(
containerColor = MaterialTheme.colorScheme.primary, containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary contentColor = MaterialTheme.colorScheme.onPrimary
@ -709,67 +728,102 @@ fun AutoScrollControls(
Icon( Icon(
imageVector = if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow, imageVector = if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow,
contentDescription = if (isPlaying) "Pause" else "Play", 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 { } 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( IconButton(
onClick = onClose, onClick = onClose,
modifier = Modifier.size(40.dp) modifier = Modifier.size(32.dp)
) { ) {
Icon( Icon(
imageVector = Icons.Default.Close, imageVector = Icons.Default.Close,
contentDescription = "Close", contentDescription = "Close",
tint = MaterialTheme.colorScheme.error, tint = MaterialTheme.colorScheme.error,
modifier = Modifier.size(20.dp) modifier = Modifier.size(18.dp)
) )
} }
}
}
Box( Spacer(Modifier.height(16.dp))
modifier = Modifier
.width(1.dp)
.height(24.dp)
.background(MaterialTheme.colorScheme.outlineVariant)
)
// Bottom Row: Primary Controls
Row( Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(0.dp) horizontalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
IconButton( // 1. Play/Pause Button
onClick = { onSpeedChange((speed - 0.1f).coerceAtLeast(0.1f)) }, Box(modifier = Modifier.size(48.dp), contentAlignment = Alignment.Center) {
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)
)
FilledIconButton( FilledIconButton(
onClick = onPlayPauseToggle, onClick = onPlayPauseToggle,
modifier = Modifier.size(40.dp), modifier = Modifier.size(48.dp),
colors = IconButtonDefaults.filledIconButtonColors( colors = IconButtonDefaults.filledIconButtonColors(
containerColor = MaterialTheme.colorScheme.primary, containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary contentColor = MaterialTheme.colorScheme.onPrimary
@ -781,16 +835,86 @@ fun AutoScrollControls(
modifier = Modifier.size(24.dp) modifier = Modifier.size(24.dp)
) )
} }
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( IconButton(
onClick = { onCollapseChange(true) }, onClick = { onSpeedChange((speed + 0.1f).coerceAtMost(10f)) },
modifier = Modifier.size(40.dp) modifier = Modifier.size(48.dp)
) { ) {
Icon( Icon(Icons.Default.Add, "Faster")
imageVector = Icons.Default.ChevronRight, }
contentDescription = "Collapse", }
tint = MaterialTheme.colorScheme.onSurfaceVariant }
) }
}
} }
} }
} }

View file

@ -17,6 +17,7 @@
* *
* mail: epistemereader@gmail.com * mail: epistemereader@gmail.com
*/ */
// EpubReaderScreen.kt
@file:OptIn(ExperimentalSerializationApi::class) @file:Suppress("VariableNeverRead") @file:OptIn(ExperimentalSerializationApi::class) @file:Suppress("VariableNeverRead")
package com.aryan.reader.epubreader package com.aryan.reader.epubreader
@ -171,6 +172,30 @@ import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import kotlin.math.roundToInt import kotlin.math.roundToInt
import androidx.compose.ui.BiasAlignment 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) @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@Composable @Composable
@ -607,9 +632,14 @@ fun EpubReaderHost(
var isAutoScrollModeActive by remember { mutableStateOf(false) } var isAutoScrollModeActive by remember { mutableStateOf(false) }
var isAutoScrollPlaying 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 autoScrollSpeed by remember { mutableFloatStateOf(loadAutoScrollSpeed(context)) }
var isAutoScrollCollapsed by remember { mutableStateOf(false) } var isAutoScrollCollapsed by remember { mutableStateOf(false) }
var isAutoScrollLocked by remember { mutableStateOf(loadAutoScrollLocked(context)) }
var autoScrollUseSlider by remember { mutableStateOf(loadAutoScrollUseSlider(context)) }
DisposableEffect(Unit) { DisposableEffect(Unit) {
onDispose { onDispose {
Timber.d("Disposing sample MediaPlayer.") Timber.d("Disposing sample MediaPlayer.")
@ -618,10 +648,28 @@ fun EpubReaderHost(
} }
fun updateAutoScrollState(playing: Boolean, speed: Float) { 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) { if (isAutoScrollModeActive) {
updateAutoScrollState(isAutoScrollPlaying, autoScrollSpeed) updateAutoScrollState(isAutoScrollPlaying, autoScrollSpeed)
} else { } else {
@ -629,12 +677,6 @@ fun EpubReaderHost(
} }
} }
fun pauseAutoScroll() {
if (isAutoScrollModeActive && isAutoScrollPlaying) {
isAutoScrollPlaying = false
}
}
fun startTts() { fun startTts() {
if (isAutoScrollModeActive) { if (isAutoScrollModeActive) {
isAutoScrollModeActive = false isAutoScrollModeActive = false
@ -1615,11 +1657,8 @@ fun EpubReaderHost(
} }
if (isAutoScrollModeActive && isAutoScrollPlaying) { if (isAutoScrollModeActive && isAutoScrollPlaying) {
Timber.d("Continuing Auto-Scroll for new chapter.") Timber.d("Continuing Auto-Scroll for new chapter with delay.")
scope.launch { triggerAutoScrollTempPause(1000L)
delay(300)
updateAutoScrollState(true, autoScrollSpeed)
}
} }
}, },
onTap = { onTap = {
@ -1637,7 +1676,6 @@ fun EpubReaderHost(
Timber.d("Chapter tapped, showing main bars.") Timber.d("Chapter tapped, showing main bars.")
} }
} }
pauseAutoScroll()
}, },
onPotentialScroll = { onPotentialScroll = {
if (showBars) { if (showBars) {
@ -1645,7 +1683,9 @@ fun EpubReaderHost(
showFormatAdjustmentBars = false showFormatAdjustmentBars = false
Timber.d("Scroll/Drag detected, hiding bars.") Timber.d("Scroll/Drag detected, hiding bars.")
} }
pauseAutoScroll() if (isAutoScrollModeActive && isAutoScrollPlaying) {
triggerAutoScrollTempPause(1000L)
}
}, },
onAutoScrollChapterEnd = { onAutoScrollChapterEnd = {
Timber.d("Screen: onAutoScrollChapterEnd triggered. Current Index: $currentChapterIndex") Timber.d("Screen: onAutoScrollChapterEnd triggered. Current Index: $currentChapterIndex")
@ -2620,6 +2660,7 @@ fun EpubReaderHost(
isAutoScrollModeActive = true isAutoScrollModeActive = true
isAutoScrollPlaying = true isAutoScrollPlaying = true
showBars = false showBars = false
showBars = true
}, },
searchFocusRequester = searchFocusRequester, searchFocusRequester = searchFocusRequester,
modifier = Modifier.align(Alignment.TopCenter) modifier = Modifier.align(Alignment.TopCenter)
@ -2635,8 +2676,10 @@ fun EpubReaderHost(
label = "AutoScrollAlignAnimation" label = "AutoScrollAlignAnimation"
) )
val isAutoScrollControlsVisible = isAutoScrollModeActive && (!isAutoScrollLocked || showBars)
AnimatedVisibility( AnimatedVisibility(
visible = isAutoScrollModeActive, visible = isAutoScrollControlsVisible,
enter = slideInVertically { it } + fadeIn(), enter = slideInVertically { it } + fadeIn(),
exit = slideOutVertically { it } + fadeOut(), exit = slideOutVertically { it } + fadeOut(),
modifier = Modifier modifier = Modifier
@ -2646,8 +2689,19 @@ fun EpubReaderHost(
) { ) {
AutoScrollControls( AutoScrollControls(
isPlaying = isAutoScrollPlaying, isPlaying = isAutoScrollPlaying,
onPlayPauseToggle = { isAutoScrollPlaying = !isAutoScrollPlaying }, isTempPaused = isAutoScrollTempPaused,
onPlayPauseToggle = {
if (isAutoScrollPlaying) {
isAutoScrollPlaying = false
isAutoScrollTempPaused = false
autoScrollResumeJob.value?.cancel()
} else {
isAutoScrollPlaying = true
isAutoScrollTempPaused = false
}
},
speed = autoScrollSpeed, speed = autoScrollSpeed,
maxSpeed = 10f,
onSpeedChange = { onSpeedChange = {
autoScrollSpeed = it autoScrollSpeed = it
saveAutoScrollSpeed(context, it) saveAutoScrollSpeed(context, it)
@ -2658,7 +2712,17 @@ fun EpubReaderHost(
showBars = true showBars = true
}, },
isCollapsed = isAutoScrollCollapsed, 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, topContentPaddingPx: Float = 0f,
onTextBoxMoved: (String, Int, Rect) -> Unit = { _, _, _ -> }, onTextBoxMoved: (String, Int, Rect) -> Unit = { _, _, _ -> },
isAutoScrollPlaying: Boolean = false, isAutoScrollPlaying: Boolean = false,
isAutoScrollTempPaused: Boolean = false,
autoScrollSpeed: Float = 1.0f, autoScrollSpeed: Float = 1.0f,
onInteractionListener: () -> Unit = {} onInteractionListener: () -> Unit = {}
) { ) {
@ -484,9 +485,9 @@ internal fun PdfVerticalReader(
} }
} }
LaunchedEffect(isAutoScrollPlaying, autoScrollSpeed, totalDocHeight, screenHeight) { LaunchedEffect(isAutoScrollPlaying, isAutoScrollTempPaused, autoScrollSpeed, totalDocHeight, screenHeight) {
if (isAutoScrollPlaying) { if (isAutoScrollPlaying && !isAutoScrollTempPaused) {
val baseSpeedPxPerSec = 30f val baseSpeedPxPerSec = 80f
var lastFrameTime = withFrameNanos { it } var lastFrameTime = withFrameNanos { it }
while (isActive) { 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_X_KEY = "dock_offset_x"
private const val DOCK_OFFSET_Y_KEY = "dock_offset_y" 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_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) { private fun savePdfAutoScrollSpeed(context: Context, speed: Float) {
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE) val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
@ -604,13 +626,30 @@ fun PdfViewerScreen(
var isAutoScrollModeActive by remember { mutableStateOf(false) } var isAutoScrollModeActive by remember { mutableStateOf(false) }
var isAutoScrollPlaying 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 autoScrollSpeed by remember { mutableFloatStateOf(loadPdfAutoScrollSpeed(context)) }
var isAutoScrollCollapsed by remember { mutableStateOf(false) } 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 { val onAutoScrollInteraction = remember {
{ {
if (isAutoScrollPlaying) { if (isAutoScrollPlaying) {
isAutoScrollPlaying = false triggerAutoScrollTempPause(1000L)
} }
} }
} }
@ -1102,7 +1141,6 @@ fun PdfViewerScreen(
val onSingleTapStable = remember { val onSingleTapStable = remember {
{ {
onAutoScrollInteraction()
if (selectedTextBoxId != null) { if (selectedTextBoxId != null) {
val box = textBoxes.find { it.id == selectedTextBoxId } val box = textBoxes.find { it.id == selectedTextBoxId }
if (box != null && box.text.trim().isEmpty()) { if (box != null && box.text.trim().isEmpty()) {
@ -3641,6 +3679,7 @@ fun PdfViewerScreen(
} }
}, },
isAutoScrollPlaying = isAutoScrollPlaying, isAutoScrollPlaying = isAutoScrollPlaying,
isAutoScrollTempPaused = isAutoScrollTempPaused,
autoScrollSpeed = autoScrollSpeed, autoScrollSpeed = autoScrollSpeed,
onInteractionListener = onAutoScrollInteraction onInteractionListener = onAutoScrollInteraction
) )
@ -4116,7 +4155,7 @@ fun PdfViewerScreen(
showMoreMenu = false showMoreMenu = false
isAutoScrollModeActive = true isAutoScrollModeActive = true
isAutoScrollPlaying = true isAutoScrollPlaying = true
showBars = false showBars = true
} }
) )
HorizontalDivider() HorizontalDivider()
@ -5554,13 +5593,15 @@ fun PdfViewerScreen(
label = "AutoScrollPadding" label = "AutoScrollPadding"
) )
val isAutoScrollControlsVisible = isAutoScrollModeActive && (!isAutoScrollLocked || showBars)
val alignmentBias by animateFloatAsState( val alignmentBias by animateFloatAsState(
targetValue = if (isAutoScrollCollapsed) 1f else 0f, targetValue = if (isAutoScrollCollapsed) 1f else 0f,
label = "AutoScrollAlignAnimation" label = "AutoScrollAlignAnimation"
) )
AnimatedVisibility( AnimatedVisibility(
visible = isAutoScrollModeActive, visible = isAutoScrollControlsVisible,
enter = slideInVertically { it } + fadeIn(), enter = slideInVertically { it } + fadeIn(),
exit = slideOutVertically { it } + fadeOut(), exit = slideOutVertically { it } + fadeOut(),
modifier = Modifier modifier = Modifier
@ -5570,8 +5611,10 @@ fun PdfViewerScreen(
) { ) {
AutoScrollControls( AutoScrollControls(
isPlaying = isAutoScrollPlaying, isPlaying = isAutoScrollPlaying,
isTempPaused = isAutoScrollTempPaused,
onPlayPauseToggle = { isAutoScrollPlaying = !isAutoScrollPlaying }, onPlayPauseToggle = { isAutoScrollPlaying = !isAutoScrollPlaying },
speed = autoScrollSpeed, speed = autoScrollSpeed,
maxSpeed = 20f,
onSpeedChange = { onSpeedChange = {
autoScrollSpeed = it autoScrollSpeed = it
savePdfAutoScrollSpeed(context, it) savePdfAutoScrollSpeed(context, it)
@ -5582,7 +5625,17 @@ fun PdfViewerScreen(
showBars = true showBars = true
}, },
isCollapsed = isAutoScrollCollapsed, isCollapsed = isAutoScrollCollapsed,
onCollapseChange = { isAutoScrollCollapsed = it } onCollapseChange = { isAutoScrollCollapsed = it },
isLocked = isAutoScrollLocked,
onLockToggle = {
isAutoScrollLocked = !isAutoScrollLocked
savePdfAutoScrollLocked(context, isAutoScrollLocked)
},
useSlider = autoScrollUseSlider,
onInputModeToggle = {
autoScrollUseSlider = !autoScrollUseSlider
savePdfAutoScrollUseSlider(context, autoScrollUseSlider)
}
) )
} }
} }