diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 358a1a6..53163a2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -25,8 +25,8 @@ android { applicationId = "com.aryan.reader" minSdk = 26 targetSdk = 35 - versionCode = 33 - versionName = "1.0.32" + versionCode = 34 + versionName = "1.0.33" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" externalNativeBuild { diff --git a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderControls.kt b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderControls.kt index f29197c..bb7d9f9 100644 --- a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderControls.kt +++ b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderControls.kt @@ -660,13 +660,49 @@ suspend fun captureWebViewVisibleArea(webView: WebView): Bitmap? { } } +@Composable +fun SpeedDropdown( + label: String, + currentValue: Float, + options: List, + 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) @Composable fun AutoScrollControls( isPlaying: Boolean, onPlayPauseToggle: () -> Unit, speed: Float, + minSpeed: Float, + maxSpeed: Float, onSpeedChange: (Float) -> Unit, + onMinSpeedChange: (Float) -> Unit, + onMaxSpeedChange: (Float) -> Unit, onClose: () -> Unit, isCollapsed: Boolean, onCollapseChange: (Boolean) -> Unit, @@ -675,7 +711,6 @@ fun AutoScrollControls( useSlider: Boolean, onInputModeToggle: () -> Unit, modifier: Modifier = Modifier, - maxSpeed: Float = 10f, isTempPaused: Boolean = false, ) { Surface( @@ -849,67 +884,91 @@ fun AutoScrollControls( modifier = Modifier.weight(1f), contentAlignment = Alignment.Center ) { - if (useSlider) { - // Slider Mode: Text + Slider + Column(horizontalAlignment = Alignment.CenterHorizontally) { + val speedOptions = listOf(0.1f, 1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 10f) + // Min/Max Dropdowns Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp) + modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween ) { - Text( - text = "%.1fx".format(speed), - style = MaterialTheme.typography.titleMedium.copy(fontFeatureSettings = "tnum"), - modifier = Modifier.width(45.dp), - textAlign = TextAlign.End + SpeedDropdown( + label = "Min", + currentValue = minSpeed, + options = speedOptions, + onValueChange = onMinSpeedChange ) - 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 - ) {} - } + SpeedDropdown( + label = "Max", + currentValue = maxSpeed, + options = speedOptions, + onValueChange = onMaxSpeedChange ) } - } 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") - } + Spacer(Modifier.height(4.dp)) + val safeMax = maxSpeed.coerceAtLeast(minSpeed + 0.1f) + + 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"), - textAlign = TextAlign.Center + modifier = Modifier.width(45.dp), + textAlign = TextAlign.End ) + val steps = ((safeMax - minSpeed) / 0.1f).roundToInt() - 1 - IconButton( - onClick = { onSpeedChange((speed + 0.1f).coerceAtMost(10f)) }, - modifier = Modifier.size(48.dp) + Slider( + value = speed, + onValueChange = { onSpeedChange((it * 10f).roundToInt() / 10f) }, + valueRange = minSpeed..safeMax, + 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 ) { - Icon(Icons.Default.Add, "Faster") + IconButton( + onClick = { onSpeedChange((speed - 0.1f).coerceAtLeast(minSpeed)) }, + 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(safeMax)) }, + modifier = Modifier.size(48.dp) + ) { + Icon(Icons.Default.Add, "Faster") + } } } } diff --git a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderScreen.kt b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderScreen.kt index 8e63a78..e2dbdc1 100644 --- a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderScreen.kt +++ b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderScreen.kt @@ -173,6 +173,29 @@ import kotlin.math.roundToInt 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_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) { 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 { mutableStateOf(loadHighlightPalette(context)) } @@ -570,8 +598,6 @@ fun EpubReaderHost( var isAutoScrollPlaying by remember { mutableStateOf(false) } var isAutoScrollTempPaused by remember { mutableStateOf(false) } val autoScrollResumeJob = remember { mutableStateOf(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)) } @@ -585,7 +611,7 @@ fun EpubReaderHost( fun updateAutoScrollState(playing: Boolean, speed: Float) { val effectivePlaying = playing && !isAutoScrollTempPaused - updateAutoScrollJs(webViewRefForTts, effectivePlaying, speed) + updateAutoScrollJs(webViewRefForTts, effectivePlaying, speed * 0.5f) } fun triggerAutoScrollTempPause(durationMs: Long) { @@ -1200,17 +1226,13 @@ fun EpubReaderHost( Timber.tag("BookmarkDiagnosis").d("Navigating to ${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 parts = bookmark.cfi.split('/').mapNotNull { it.toIntOrNull() } if (parts.isNotEmpty()) { val firstIndex = parts[0] - // Standard EPUB CFI: indices are 1-based steps (2, 4, 6...) (firstIndex - 2) / 2 } else null - } catch (e: Exception) { + } catch (_: Exception) { null } @@ -1234,8 +1256,6 @@ fun EpubReaderHost( if (targetChunk != null && targetChunk >= 0) { 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) { Timber.tag("BookmarkDiagnosis").d("Manual Chunk Injection: Loading from $loadedChunkCount to $targetChunk") @@ -1587,7 +1607,7 @@ fun EpubReaderHost( "ControlFlowWithEmptyBody" ) ChapterWebView( - key = "$chapterKeyForWebView", + key = chapterKeyForWebView, chapterTitle = chapterToRender.title, isDarkTheme = isDarkTheme, initialScrollTarget = initialScrollTargetForChapter, @@ -2704,11 +2724,42 @@ fun EpubReaderHost( } }, speed = autoScrollSpeed, - maxSpeed = 10f, + minSpeed = autoScrollMinSpeed, + maxSpeed = autoScrollMaxSpeed, onSpeedChange = { autoScrollSpeed = 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 = { isAutoScrollModeActive = false isAutoScrollPlaying = false diff --git a/app/src/main/java/com/aryan/reader/pdf/PdfViewerScreen.kt b/app/src/main/java/com/aryan/reader/pdf/PdfViewerScreen.kt index e816249..c10e6cf 100644 --- a/app/src/main/java/com/aryan/reader/pdf/PdfViewerScreen.kt +++ b/app/src/main/java/com/aryan/reader/pdf/PdfViewerScreen.kt @@ -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_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_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) { val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE) @@ -630,6 +652,8 @@ fun PdfViewerScreen( var isAutoScrollTempPaused by remember { mutableStateOf(false) } val autoScrollResumeJob = remember { mutableStateOf(null) } 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 isAutoScrollLocked by remember { mutableStateOf(loadPdfAutoScrollLocked(context)) } @@ -3650,7 +3674,7 @@ fun PdfViewerScreen( }, isAutoScrollPlaying = isAutoScrollPlaying, isAutoScrollTempPaused = isAutoScrollTempPaused, - autoScrollSpeed = autoScrollSpeed, + autoScrollSpeed = autoScrollSpeed * 0.5f, onInteractionListener = onAutoScrollInteraction ) } @@ -5569,11 +5593,42 @@ fun PdfViewerScreen( isTempPaused = isAutoScrollTempPaused, onPlayPauseToggle = { isAutoScrollPlaying = !isAutoScrollPlaying }, speed = autoScrollSpeed, - maxSpeed = 20f, + minSpeed = autoScrollMinSpeed, + maxSpeed = autoScrollMaxSpeed, onSpeedChange = { autoScrollSpeed = 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 = { isAutoScrollModeActive = false isAutoScrollPlaying = false