Auto scroll update (#20)

* Added configurable min/max speed settings for auto-scroll in EPUB and PDF readers

* bump version to 1.0.33 (34)
This commit is contained in:
Aryan 2026-03-02 00:07:01 +05:30 committed by GitHub
parent d6837aa4df
commit 34c6ec3fef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 231 additions and 66 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 = 33 versionCode = 34
versionName = "1.0.32" versionName = "1.0.33"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
externalNativeBuild { externalNativeBuild {

View file

@ -660,13 +660,49 @@ suspend fun captureWebViewVisibleArea(webView: WebView): Bitmap? {
} }
} }
@Composable
fun SpeedDropdown(
label: String,
currentValue: Float,
options: List<Float>,
onValueChange: (Float) -> Unit
) {
var expanded by remember { mutableStateOf(false) }
Box {
Text(
text = "$label: ${currentValue}x",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier
.clickable { expanded = true }
.padding(4.dp)
)
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
options.forEach { opt ->
DropdownMenuItem(
text = { Text("${opt}x") },
onClick = {
onValueChange(opt)
expanded = false
}
)
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AutoScrollControls( fun AutoScrollControls(
isPlaying: Boolean, isPlaying: Boolean,
onPlayPauseToggle: () -> Unit, onPlayPauseToggle: () -> Unit,
speed: Float, speed: Float,
minSpeed: Float,
maxSpeed: Float,
onSpeedChange: (Float) -> Unit, onSpeedChange: (Float) -> Unit,
onMinSpeedChange: (Float) -> Unit,
onMaxSpeedChange: (Float) -> Unit,
onClose: () -> Unit, onClose: () -> Unit,
isCollapsed: Boolean, isCollapsed: Boolean,
onCollapseChange: (Boolean) -> Unit, onCollapseChange: (Boolean) -> Unit,
@ -675,7 +711,6 @@ fun AutoScrollControls(
useSlider: Boolean, useSlider: Boolean,
onInputModeToggle: () -> Unit, onInputModeToggle: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
maxSpeed: Float = 10f,
isTempPaused: Boolean = false, isTempPaused: Boolean = false,
) { ) {
Surface( Surface(
@ -849,6 +884,30 @@ fun AutoScrollControls(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
val speedOptions = listOf(0.1f, 1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 10f)
// Min/Max Dropdowns
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
SpeedDropdown(
label = "Min",
currentValue = minSpeed,
options = speedOptions,
onValueChange = onMinSpeedChange
)
SpeedDropdown(
label = "Max",
currentValue = maxSpeed,
options = speedOptions,
onValueChange = onMaxSpeedChange
)
}
Spacer(Modifier.height(4.dp))
val safeMax = maxSpeed.coerceAtLeast(minSpeed + 0.1f)
if (useSlider) { if (useSlider) {
// Slider Mode: Text + Slider // Slider Mode: Text + Slider
Row( Row(
@ -861,13 +920,12 @@ fun AutoScrollControls(
modifier = Modifier.width(45.dp), modifier = Modifier.width(45.dp),
textAlign = TextAlign.End textAlign = TextAlign.End
) )
val minSpeed = 0.1f val steps = ((safeMax - minSpeed) / 0.1f).roundToInt() - 1
val steps = ((maxSpeed - minSpeed) / 0.1f).roundToInt() - 1
Slider( Slider(
value = speed, value = speed,
onValueChange = { onSpeedChange((it * 10f).roundToInt() / 10f) }, onValueChange = { onSpeedChange((it * 10f).roundToInt() / 10f) },
valueRange = minSpeed..maxSpeed, valueRange = minSpeed..safeMax,
steps = if (steps > 0) steps else 0, steps = if (steps > 0) steps else 0,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
thumb = { thumb = {
@ -893,7 +951,7 @@ fun AutoScrollControls(
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
IconButton( IconButton(
onClick = { onSpeedChange((speed - 0.1f).coerceAtLeast(0.1f)) }, onClick = { onSpeedChange((speed - 0.1f).coerceAtLeast(minSpeed)) },
modifier = Modifier.size(48.dp) modifier = Modifier.size(48.dp)
) { ) {
Icon(Icons.Default.Remove, "Slower") Icon(Icons.Default.Remove, "Slower")
@ -906,7 +964,7 @@ fun AutoScrollControls(
) )
IconButton( IconButton(
onClick = { onSpeedChange((speed + 0.1f).coerceAtMost(10f)) }, onClick = { onSpeedChange((speed + 0.1f).coerceAtMost(safeMax)) },
modifier = Modifier.size(48.dp) modifier = Modifier.size(48.dp)
) { ) {
Icon(Icons.Default.Add, "Faster") Icon(Icons.Default.Add, "Faster")
@ -921,3 +979,4 @@ fun AutoScrollControls(
} }
} }
} }
}

View file

@ -173,6 +173,29 @@ import kotlin.math.roundToInt
private const val AUTO_SCROLL_LOCKED_KEY = "auto_scroll_locked" private const val AUTO_SCROLL_LOCKED_KEY = "auto_scroll_locked"
private const val AUTO_SCROLL_USE_SLIDER_KEY = "auto_scroll_use_slider" private const val AUTO_SCROLL_USE_SLIDER_KEY = "auto_scroll_use_slider"
private const val AUTO_SCROLL_MIN_SPEED_KEY = "auto_scroll_min_speed"
private const val AUTO_SCROLL_MAX_SPEED_KEY = "auto_scroll_max_speed"
private fun saveAutoScrollMinSpeed(context: Context, speed: Float) {
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
prefs.edit { putFloat(AUTO_SCROLL_MIN_SPEED_KEY, speed) }
}
private fun loadAutoScrollMinSpeed(context: Context): Float {
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
return prefs.getFloat(AUTO_SCROLL_MIN_SPEED_KEY, 0.1f)
}
private fun saveAutoScrollMaxSpeed(context: Context, speed: Float) {
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
prefs.edit { putFloat(AUTO_SCROLL_MAX_SPEED_KEY, speed) }
}
private fun loadAutoScrollMaxSpeed(context: Context): Float {
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
return prefs.getFloat(AUTO_SCROLL_MAX_SPEED_KEY, 10.0f)
}
private fun saveAutoScrollLocked(context: Context, isLocked: Boolean) { private fun saveAutoScrollLocked(context: Context, isLocked: Boolean) {
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE) val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
@ -300,6 +323,11 @@ fun EpubReaderHost(
} }
} }
var autoScrollSpeed by remember { mutableFloatStateOf(loadAutoScrollSpeed(context)) }
var autoScrollMinSpeed by remember { mutableFloatStateOf(loadAutoScrollMinSpeed(context)) }
var autoScrollMaxSpeed by remember { mutableFloatStateOf(loadAutoScrollMaxSpeed(context)) }
var isAutoScrollCollapsed by remember { mutableStateOf(false) }
var currentHighlightPalette by remember { var currentHighlightPalette by remember {
mutableStateOf(loadHighlightPalette(context)) mutableStateOf(loadHighlightPalette(context))
} }
@ -570,8 +598,6 @@ fun EpubReaderHost(
var isAutoScrollPlaying by remember { mutableStateOf(false) } var isAutoScrollPlaying by remember { mutableStateOf(false) }
var isAutoScrollTempPaused by remember { mutableStateOf(false) } var isAutoScrollTempPaused by remember { mutableStateOf(false) }
val autoScrollResumeJob = remember { mutableStateOf<Job?>(null) } 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 isAutoScrollLocked by remember { mutableStateOf(loadAutoScrollLocked(context)) }
var autoScrollUseSlider by remember { mutableStateOf(loadAutoScrollUseSlider(context)) } var autoScrollUseSlider by remember { mutableStateOf(loadAutoScrollUseSlider(context)) }
@ -585,7 +611,7 @@ fun EpubReaderHost(
fun updateAutoScrollState(playing: Boolean, speed: Float) { fun updateAutoScrollState(playing: Boolean, speed: Float) {
val effectivePlaying = playing && !isAutoScrollTempPaused val effectivePlaying = playing && !isAutoScrollTempPaused
updateAutoScrollJs(webViewRefForTts, effectivePlaying, speed) updateAutoScrollJs(webViewRefForTts, effectivePlaying, speed * 0.5f)
} }
fun triggerAutoScrollTempPause(durationMs: Long) { fun triggerAutoScrollTempPause(durationMs: Long) {
@ -1200,17 +1226,13 @@ fun EpubReaderHost(
Timber.tag("BookmarkDiagnosis").d("Navigating to ${bookmark.cfi}") Timber.tag("BookmarkDiagnosis").d("Navigating to ${bookmark.cfi}")
cfiToLoad = bookmark.cfi cfiToLoad = bookmark.cfi
// FIX: Try to extract chunk index directly from CFI for Vertical Mode
// Vertical Mode CFIs are relative to content-container, so the first number
// usually represents the chunk (2->Chunk0, 4->Chunk1, 6->Chunk2...)
val directChunkIndex = try { val directChunkIndex = try {
val parts = bookmark.cfi.split('/').mapNotNull { it.toIntOrNull() } val parts = bookmark.cfi.split('/').mapNotNull { it.toIntOrNull() }
if (parts.isNotEmpty()) { if (parts.isNotEmpty()) {
val firstIndex = parts[0] val firstIndex = parts[0]
// Standard EPUB CFI: indices are 1-based steps (2, 4, 6...)
(firstIndex - 2) / 2 (firstIndex - 2) / 2
} else null } else null
} catch (e: Exception) { } catch (_: Exception) {
null null
} }
@ -1234,8 +1256,6 @@ fun EpubReaderHost(
if (targetChunk != null && targetChunk >= 0) { if (targetChunk != null && targetChunk >= 0) {
isNavigatingToBookmark = true isNavigatingToBookmark = true
// FIX: Ensure we don't reload if we already have it,
// but do ensure the WebView has the content injected.
if (targetChunk >= loadedChunkCount) { if (targetChunk >= loadedChunkCount) {
Timber.tag("BookmarkDiagnosis").d("Manual Chunk Injection: Loading from $loadedChunkCount to $targetChunk") Timber.tag("BookmarkDiagnosis").d("Manual Chunk Injection: Loading from $loadedChunkCount to $targetChunk")
@ -1587,7 +1607,7 @@ fun EpubReaderHost(
"ControlFlowWithEmptyBody" "ControlFlowWithEmptyBody"
) )
ChapterWebView( ChapterWebView(
key = "$chapterKeyForWebView", key = chapterKeyForWebView,
chapterTitle = chapterToRender.title, chapterTitle = chapterToRender.title,
isDarkTheme = isDarkTheme, isDarkTheme = isDarkTheme,
initialScrollTarget = initialScrollTargetForChapter, initialScrollTarget = initialScrollTargetForChapter,
@ -2704,11 +2724,42 @@ fun EpubReaderHost(
} }
}, },
speed = autoScrollSpeed, speed = autoScrollSpeed,
maxSpeed = 10f, minSpeed = autoScrollMinSpeed,
maxSpeed = autoScrollMaxSpeed,
onSpeedChange = { onSpeedChange = {
autoScrollSpeed = it autoScrollSpeed = it
saveAutoScrollSpeed(context, it) saveAutoScrollSpeed(context, it)
}, },
onMinSpeedChange = { newMin ->
autoScrollMinSpeed = newMin
saveAutoScrollMinSpeed(context, newMin)
if (autoScrollMaxSpeed < newMin) {
autoScrollMaxSpeed = newMin
saveAutoScrollMaxSpeed(context, newMin)
}
if (autoScrollSpeed < newMin) {
autoScrollSpeed = newMin
saveAutoScrollSpeed(context, newMin)
} else if (autoScrollSpeed > autoScrollMaxSpeed) {
autoScrollSpeed = autoScrollMaxSpeed
saveAutoScrollSpeed(context, autoScrollMaxSpeed)
}
},
onMaxSpeedChange = { newMax ->
autoScrollMaxSpeed = newMax
saveAutoScrollMaxSpeed(context, newMax)
if (autoScrollMinSpeed > newMax) {
autoScrollMinSpeed = newMax
saveAutoScrollMinSpeed(context, newMax)
}
if (autoScrollSpeed > newMax) {
autoScrollSpeed = newMax
saveAutoScrollSpeed(context, newMax)
} else if (autoScrollSpeed < autoScrollMinSpeed) {
autoScrollSpeed = autoScrollMinSpeed
saveAutoScrollSpeed(context, autoScrollMinSpeed)
}
},
onClose = { onClose = {
isAutoScrollModeActive = false isAutoScrollModeActive = false
isAutoScrollPlaying = false isAutoScrollPlaying = false

View file

@ -288,6 +288,28 @@ 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_LOCKED_KEY = "pdf_auto_scroll_locked"
private const val PDF_AUTO_SCROLL_USE_SLIDER_KEY = "pdf_auto_scroll_use_slider" private const val PDF_AUTO_SCROLL_USE_SLIDER_KEY = "pdf_auto_scroll_use_slider"
private const val PDF_AUTO_SCROLL_MIN_SPEED_KEY = "pdf_auto_scroll_min_speed"
private const val PDF_AUTO_SCROLL_MAX_SPEED_KEY = "pdf_auto_scroll_max_speed"
private fun savePdfAutoScrollMinSpeed(context: Context, speed: Float) {
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
prefs.edit { putFloat(PDF_AUTO_SCROLL_MIN_SPEED_KEY, speed) }
}
private fun loadPdfAutoScrollMinSpeed(context: Context): Float {
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
return prefs.getFloat(PDF_AUTO_SCROLL_MIN_SPEED_KEY, 0.1f)
}
private fun savePdfAutoScrollMaxSpeed(context: Context, speed: Float) {
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
prefs.edit { putFloat(PDF_AUTO_SCROLL_MAX_SPEED_KEY, speed) }
}
private fun loadPdfAutoScrollMaxSpeed(context: Context): Float {
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
return prefs.getFloat(PDF_AUTO_SCROLL_MAX_SPEED_KEY, 10.0f)
}
private fun savePdfAutoScrollLocked(context: Context, isLocked: Boolean) { private fun savePdfAutoScrollLocked(context: Context, isLocked: Boolean) {
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE) val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
@ -630,6 +652,8 @@ fun PdfViewerScreen(
var isAutoScrollTempPaused by remember { mutableStateOf(false) } var isAutoScrollTempPaused by remember { mutableStateOf(false) }
val autoScrollResumeJob = remember { mutableStateOf<Job?>(null) } val autoScrollResumeJob = remember { mutableStateOf<Job?>(null) }
var autoScrollSpeed by remember { mutableFloatStateOf(loadPdfAutoScrollSpeed(context)) } var autoScrollSpeed by remember { mutableFloatStateOf(loadPdfAutoScrollSpeed(context)) }
var autoScrollMinSpeed by remember { mutableFloatStateOf(loadPdfAutoScrollMinSpeed(context)) }
var autoScrollMaxSpeed by remember { mutableFloatStateOf(loadPdfAutoScrollMaxSpeed(context)) }
var isAutoScrollCollapsed by remember { mutableStateOf(false) } var isAutoScrollCollapsed by remember { mutableStateOf(false) }
var isAutoScrollLocked by remember { mutableStateOf(loadPdfAutoScrollLocked(context)) } var isAutoScrollLocked by remember { mutableStateOf(loadPdfAutoScrollLocked(context)) }
@ -3650,7 +3674,7 @@ fun PdfViewerScreen(
}, },
isAutoScrollPlaying = isAutoScrollPlaying, isAutoScrollPlaying = isAutoScrollPlaying,
isAutoScrollTempPaused = isAutoScrollTempPaused, isAutoScrollTempPaused = isAutoScrollTempPaused,
autoScrollSpeed = autoScrollSpeed, autoScrollSpeed = autoScrollSpeed * 0.5f,
onInteractionListener = onAutoScrollInteraction onInteractionListener = onAutoScrollInteraction
) )
} }
@ -5569,11 +5593,42 @@ fun PdfViewerScreen(
isTempPaused = isAutoScrollTempPaused, isTempPaused = isAutoScrollTempPaused,
onPlayPauseToggle = { isAutoScrollPlaying = !isAutoScrollPlaying }, onPlayPauseToggle = { isAutoScrollPlaying = !isAutoScrollPlaying },
speed = autoScrollSpeed, speed = autoScrollSpeed,
maxSpeed = 20f, minSpeed = autoScrollMinSpeed,
maxSpeed = autoScrollMaxSpeed,
onSpeedChange = { onSpeedChange = {
autoScrollSpeed = it autoScrollSpeed = it
savePdfAutoScrollSpeed(context, it) savePdfAutoScrollSpeed(context, it)
}, },
onMinSpeedChange = { newMin ->
autoScrollMinSpeed = newMin
savePdfAutoScrollMinSpeed(context, newMin)
if (autoScrollMaxSpeed < newMin) {
autoScrollMaxSpeed = newMin
savePdfAutoScrollMaxSpeed(context, newMin)
}
if (autoScrollSpeed < newMin) {
autoScrollSpeed = newMin
savePdfAutoScrollSpeed(context, newMin)
} else if (autoScrollSpeed > autoScrollMaxSpeed) {
autoScrollSpeed = autoScrollMaxSpeed
savePdfAutoScrollSpeed(context, autoScrollMaxSpeed)
}
},
onMaxSpeedChange = { newMax ->
autoScrollMaxSpeed = newMax
savePdfAutoScrollMaxSpeed(context, newMax)
if (autoScrollMinSpeed > newMax) {
autoScrollMinSpeed = newMax
savePdfAutoScrollMinSpeed(context, newMax)
}
if (autoScrollSpeed > newMax) {
autoScrollSpeed = newMax
savePdfAutoScrollSpeed(context, newMax)
} else if (autoScrollSpeed < autoScrollMinSpeed) {
autoScrollSpeed = autoScrollMinSpeed
savePdfAutoScrollSpeed(context, autoScrollMinSpeed)
}
},
onClose = { onClose = {
isAutoScrollModeActive = false isAutoScrollModeActive = false
isAutoScrollPlaying = false isAutoScrollPlaying = false