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:
parent
1f111034f1
commit
de376e2ace
6 changed files with 410 additions and 238 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
||||
,
|
||||
}
|
||||
|
||||
;
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue