Auto scroll update (#28)

* Updated auto-scroll controls UI and refined pause behavior.

- Added semi-transparent look to auto-scroll
- Reduced auto-scroll temporary pause duration from 1000ms to 300ms.
- Updated `AutoScrollControls` styling with background transparency and adjusted elevations/borders.
- Refined `CircularProgressIndicator` visibility to only show when active and paused.
- Enhanced the `Slider` appearance in `AutoScrollControls` with a custom track and thumb.
- Improved play/pause logic in `PdfViewerScreen` to properly reset temporary pause states and jobs.
- Cleaned up redundant comments and formatting in `EpubReaderControls.kt`.

* feat: add file-specific auto-scroll speed profiles for EPUB and PDF

- Updated AutoScrollControls UI with a dropdown to toggle between Global and Local speed modes.
- Implemented persistent local auto-scroll settings (Current, Min, and Max speed) keyed by book ID.
- Ensured "Local mode" preference is sticky per-file, while maintaining Global defaults for new files.

* Enabled auto-scroll toggling via single tap in EPUB and PDF readers
This commit is contained in:
Aryan 2026-03-04 16:40:02 +05:30 committed by GitHub
parent b47b6a9164
commit 0b7bb53a28
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 435 additions and 107 deletions

View file

@ -68,6 +68,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.ChevronLeft
import androidx.compose.material.icons.filled.ChevronRight
@ -102,6 +103,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.graphicsLayer
@ -740,7 +742,6 @@ fun SpeedDropdown(
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AutoScrollControls(
@ -759,15 +760,19 @@ fun AutoScrollControls(
onLockToggle: () -> Unit,
useSlider: Boolean,
onInputModeToggle: () -> Unit,
isLocalMode: Boolean,
onLocalModeToggle: (Boolean) -> Unit,
modifier: Modifier = Modifier,
isTempPaused: Boolean = false,
) {
val backgroundAlpha = 0.6f
Surface(
shape = RoundedCornerShape(28.dp),
color = MaterialTheme.colorScheme.surfaceContainerHigh,
tonalElevation = 8.dp,
shadowElevation = 6.dp,
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)),
color = MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = backgroundAlpha),
tonalElevation = 0.dp,
shadowElevation = 0.dp,
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.8f)),
modifier = modifier
.widthIn(max = 400.dp)
.animateContentSize()
@ -780,14 +785,12 @@ fun AutoScrollControls(
label = "AutoScrollUnified"
) { 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(
onClick = { onCollapseChange(false) },
modifier = Modifier.size(36.dp)
@ -799,7 +802,6 @@ fun AutoScrollControls(
)
}
// Play/Pause Mini
Box(modifier = Modifier.size(36.dp), contentAlignment = Alignment.Center) {
FilledIconButton(
onClick = onPlayPauseToggle,
@ -815,7 +817,7 @@ fun AutoScrollControls(
modifier = Modifier.size(20.dp)
)
}
if (isTempPaused) {
if (isTempPaused && isPlaying) {
CircularProgressIndicator(
modifier = Modifier.size(36.dp),
color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.5f),
@ -825,26 +827,76 @@ fun AutoScrollControls(
}
}
} else {
// EXPANDED STATE: Card-like Layout
Column(
modifier = Modifier.padding(16.dp)
) {
// Top Row: Utility Actions (Right aligned)
// Top Row: Label & Tools
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
// Label
Text(
text = "Auto Scroll",
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary
)
Box {
var showModeMenu by remember { mutableStateOf(false) }
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(8.dp))
.clickable { showModeMenu = true }
.padding(4.dp)
) {
Text(
text = if (isLocalMode) "Local Speed" else "Global Speed",
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary
)
Icon(
imageVector = Icons.Default.ArrowDropDown,
contentDescription = "Select Mode",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(20.dp)
)
}
DropdownMenu(
expanded = showModeMenu,
onDismissRequest = { showModeMenu = false }
) {
DropdownMenuItem(
text = {
Column {
Text("Global Speed", style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold)
Text("Applies to all files", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
},
onClick = {
onLocalModeToggle(false)
showModeMenu = false
},
trailingIcon = {
if (!isLocalMode) Icon(Icons.Default.Check, contentDescription = null)
}
)
HorizontalDivider()
DropdownMenuItem(
text = {
Column {
Text("Local Speed", style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold)
Text("Saved for this file only", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
},
onClick = {
onLocalModeToggle(true)
showModeMenu = false
},
trailingIcon = {
if (isLocalMode) Icon(Icons.Default.Check, contentDescription = null)
}
)
}
}
// Tools Row
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
// Lock
IconButton(
onClick = onLockToggle,
modifier = Modifier.size(32.dp)
@ -856,7 +908,6 @@ fun AutoScrollControls(
tint = if (isLocked) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
)
}
// Swap Input
IconButton(
onClick = onInputModeToggle,
modifier = Modifier.size(32.dp)
@ -868,7 +919,6 @@ fun AutoScrollControls(
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
// Collapse
IconButton(
onClick = { onCollapseChange(true) },
modifier = Modifier.size(32.dp)
@ -880,7 +930,6 @@ fun AutoScrollControls(
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
// Close
IconButton(
onClick = onClose,
modifier = Modifier.size(32.dp)
@ -897,13 +946,13 @@ fun AutoScrollControls(
Spacer(Modifier.height(16.dp))
// Bottom Row: Primary Controls
// Bottom Row: Controls
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
// 1. Play/Pause Button
// Play/Pause
Box(modifier = Modifier.size(48.dp), contentAlignment = Alignment.Center) {
FilledIconButton(
onClick = onPlayPauseToggle,
@ -919,7 +968,7 @@ fun AutoScrollControls(
modifier = Modifier.size(24.dp)
)
}
if (isTempPaused) {
if (isTempPaused && isPlaying) {
CircularProgressIndicator(
modifier = Modifier.size(48.dp),
color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.5f),
@ -928,14 +977,13 @@ fun AutoScrollControls(
}
}
// 2. Speed Controls (Weight to fill space)
// Speed Controls
Box(
modifier = Modifier.weight(1f),
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
@ -958,7 +1006,6 @@ fun AutoScrollControls(
val safeMax = maxSpeed.coerceAtLeast(minSpeed + 0.1f)
if (useSlider) {
// Slider Mode: Text + Slider
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
@ -978,20 +1025,42 @@ fun AutoScrollControls(
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
) {}
Box(
modifier = Modifier
.size(20.dp)
.background(MaterialTheme.colorScheme.primary, CircleShape)
)
},
track = { sliderState ->
val fraction = (sliderState.value - sliderState.valueRange.start) /
(sliderState.valueRange.endInclusive - sliderState.valueRange.start)
Box(
modifier = Modifier
.fillMaxWidth()
.height(4.dp)
.background(
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.2f),
RoundedCornerShape(2.dp)
),
contentAlignment = Alignment.CenterStart
) {
Box(
modifier = Modifier
.fillMaxWidth(fraction)
.height(4.dp)
.background(
MaterialTheme.colorScheme.primary,
RoundedCornerShape(2.dp)
)
)
}
}
)
}
} else {
// Stepper Mode: Segmented Pill
Surface(
shape = RoundedCornerShape(50),
color = MaterialTheme.colorScheme.surfaceVariant,
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
modifier = Modifier.height(48.dp).fillMaxWidth()
) {
Row(
@ -1005,13 +1074,11 @@ fun AutoScrollControls(
) {
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(safeMax)) },
modifier = Modifier.size(48.dp)

View file

@ -181,6 +181,44 @@ private const val AUTO_SCROLL_MAX_SPEED_KEY = "auto_scroll_max_speed"
private const val PAGE_TURN_ANIMATION_KEY = "page_turn_animation_enabled"
private const val TTS_MODE_KEY = "tts_mode"
private const val AUTO_SCROLL_IS_LOCAL_PREFIX = "auto_scroll_is_local_"
private const val AUTO_SCROLL_LOCAL_SPEED_PREFIX = "auto_scroll_local_speed_"
private const val AUTO_SCROLL_LOCAL_MIN_PREFIX = "auto_scroll_local_min_"
private const val AUTO_SCROLL_LOCAL_MAX_PREFIX = "auto_scroll_local_max_"
private fun getBookIdForPrefs(title: String): String {
return title.hashCode().toString()
}
private fun saveAutoScrollLocalMode(context: Context, bookId: String, isLocal: Boolean) {
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
prefs.edit { putBoolean(AUTO_SCROLL_IS_LOCAL_PREFIX + bookId, isLocal) }
}
private fun loadAutoScrollLocalMode(context: Context, bookId: String): Boolean {
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
return prefs.getBoolean(AUTO_SCROLL_IS_LOCAL_PREFIX + bookId, false)
}
private fun saveAutoScrollLocalSettings(context: Context, bookId: String, speed: Float, min: Float, max: Float) {
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
prefs.edit {
putFloat(AUTO_SCROLL_LOCAL_SPEED_PREFIX + bookId, speed)
putFloat(AUTO_SCROLL_LOCAL_MIN_PREFIX + bookId, min)
putFloat(AUTO_SCROLL_LOCAL_MAX_PREFIX + bookId, max)
}
}
private fun loadAutoScrollLocalSettings(context: Context, bookId: String): Triple<Float, Float, Float>? {
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
if (!prefs.contains(AUTO_SCROLL_LOCAL_SPEED_PREFIX + bookId)) return null
val speed = prefs.getFloat(AUTO_SCROLL_LOCAL_SPEED_PREFIX + bookId, 3.0f)
val min = prefs.getFloat(AUTO_SCROLL_LOCAL_MIN_PREFIX + bookId, 0.1f)
val max = prefs.getFloat(AUTO_SCROLL_LOCAL_MAX_PREFIX + bookId, 10.0f)
return Triple(speed, min, max)
}
private fun savePageTurnAnimationSetting(context: Context, isEnabled: Boolean) {
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
prefs.edit { putBoolean(PAGE_TURN_ANIMATION_KEY, isEnabled) }
@ -349,11 +387,92 @@ 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) }
val bookId = remember(epubBook.title) { getBookIdForPrefs(epubBook.title) }
var isAutoScrollLocal by remember { mutableStateOf(loadAutoScrollLocalMode(context, bookId)) }
val initialSettings = remember(isAutoScrollLocal) {
if (isAutoScrollLocal) {
loadAutoScrollLocalSettings(context, bookId) ?: Triple(
loadAutoScrollSpeed(context),
loadAutoScrollMinSpeed(context),
loadAutoScrollMaxSpeed(context)
)
} else {
Triple(
loadAutoScrollSpeed(context),
loadAutoScrollMinSpeed(context),
loadAutoScrollMaxSpeed(context)
)
}
}
var autoScrollSpeed by remember { mutableFloatStateOf(initialSettings.first) }
var autoScrollMinSpeed by remember { mutableFloatStateOf(initialSettings.second) }
var autoScrollMaxSpeed by remember { mutableFloatStateOf(initialSettings.third) }
val onToggleAutoScrollMode = { newIsLocal: Boolean ->
isAutoScrollLocal = newIsLocal
saveAutoScrollLocalMode(context, bookId, newIsLocal)
if (newIsLocal) {
val existingLocal = loadAutoScrollLocalSettings(context, bookId)
if (existingLocal == null) {
saveAutoScrollLocalSettings(context, bookId, autoScrollSpeed, autoScrollMinSpeed, autoScrollMaxSpeed)
} else {
autoScrollSpeed = existingLocal.first
autoScrollMinSpeed = existingLocal.second
autoScrollMaxSpeed = existingLocal.third
}
} else {
autoScrollSpeed = loadAutoScrollSpeed(context)
autoScrollMinSpeed = loadAutoScrollMinSpeed(context)
autoScrollMaxSpeed = loadAutoScrollMaxSpeed(context)
}
}
val updateSpeed = { newSpeed: Float ->
autoScrollSpeed = newSpeed
if (isAutoScrollLocal) {
saveAutoScrollLocalSettings(context, bookId, newSpeed, autoScrollMinSpeed, autoScrollMaxSpeed)
} else {
saveAutoScrollSpeed(context, newSpeed)
}
}
val updateMinSpeed = { newMin: Float ->
autoScrollMinSpeed = newMin
if (isAutoScrollLocal) {
var currentMax = autoScrollMaxSpeed
var currentSpeed = autoScrollSpeed
if (currentMax < newMin) { currentMax = newMin; autoScrollMaxSpeed = newMin }
if (currentSpeed < newMin) { currentSpeed = newMin; autoScrollSpeed = newMin }
else if (currentSpeed > currentMax) { currentSpeed = currentMax; autoScrollSpeed = currentMax }
saveAutoScrollLocalSettings(context, bookId, currentSpeed, newMin, currentMax)
} else {
saveAutoScrollMinSpeed(context, newMin)
}
}
val updateMaxSpeed = { newMax: Float ->
autoScrollMaxSpeed = newMax
if (isAutoScrollLocal) {
var currentMin = autoScrollMinSpeed
var currentSpeed = autoScrollSpeed
if (currentMin > newMax) { currentMin = newMax; autoScrollMinSpeed = newMax }
if (currentSpeed > newMax) { currentSpeed = newMax; autoScrollSpeed = newMax }
else if (currentSpeed < currentMin) { currentSpeed = currentMin; autoScrollSpeed = currentMin }
saveAutoScrollLocalSettings(context, bookId, currentSpeed, currentMin, newMax)
} else {
saveAutoScrollMaxSpeed(context, newMax)
}
}
var currentHighlightPalette by remember {
mutableStateOf(loadHighlightPalette(context))
}
@ -1740,6 +1859,11 @@ fun EpubReaderHost(
}
},
onTap = {
if (isAutoScrollModeActive) {
isAutoScrollPlaying = !isAutoScrollPlaying
Timber.d("Auto-scroll toggled via tap: $isAutoScrollPlaying")
}
if (System.currentTimeMillis() - lastHighlightClickTime > 500) {
focusManager.clearFocus()
if (volumeScrollEnabled && !searchState.isSearchActive) {
@ -1762,7 +1886,7 @@ fun EpubReaderHost(
Timber.d("Scroll/Drag detected, hiding bars.")
}
if (isAutoScrollModeActive && isAutoScrollPlaying) {
triggerAutoScrollTempPause(1000L)
triggerAutoScrollTempPause(300L)
}
},
onAutoScrollChapterEnd = {
@ -2796,38 +2920,37 @@ fun EpubReaderHost(
speed = autoScrollSpeed,
minSpeed = autoScrollMinSpeed,
maxSpeed = autoScrollMaxSpeed,
onSpeedChange = {
autoScrollSpeed = it
saveAutoScrollSpeed(context, it)
},
onSpeedChange = { updateSpeed(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)
updateMinSpeed(newMin)
if (!isAutoScrollLocal) {
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)
updateMaxSpeed(newMax)
if (!isAutoScrollLocal) {
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 = {
@ -2846,7 +2969,9 @@ fun EpubReaderHost(
onInputModeToggle = {
autoScrollUseSlider = !autoScrollUseSlider
saveAutoScrollUseSlider(context, autoScrollUseSlider)
}
},
isLocalMode = isAutoScrollLocal,
onLocalModeToggle = onToggleAutoScrollMode
)
}