diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3056254..abac30e 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 = 32 - versionName = "1.0.31" + versionCode = 33 + versionName = "1.0.32" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" externalNativeBuild { diff --git a/app/src/main/assets/epub_reader.js b/app/src/main/assets/epub_reader.js index 68ecb78..ba55f6d 100644 --- a/app/src/main/assets/epub_reader.js +++ b/app/src/main/assets/epub_reader.js @@ -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'; - + this.active = false; if (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) { console.log(`$ { @@ -2710,109 +2710,39 @@ window.findFirstVisibleCfi=function (cfiArray) { , 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 subPixelRemainder=this.accumulator - pixelsToScroll; + const totalPixelsToScroll = Math.floor(this.accumulator); - if (pixelsToScroll >=1) { - const prevScrollY=window.scrollY; - window.scrollBy(0, pixelsToScroll); - this.accumulator -=pixelsToScroll; + if (totalPixelsToScroll >= 1) { + const prevScrollY = window.scrollY; + window.scrollBy(0, totalPixelsToScroll); - // Calculation logic - const innerH=window.innerHeight; - const scrollY=window.scrollY; - const docHeight=document.documentElement.scrollHeight; // Usually more reliable than body.scrollHeight + this.accumulator -= totalPixelsToScroll; - 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 scrollY = window.scrollY; + 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; - + 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)); - } - - , - } - - ; + this.animationId = requestAnimationFrame(this.loop.bind(this)); + }, + }; })(); \ No newline at end of file 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 344d57f..f29197c 100644 --- a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderControls.kt +++ b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderControls.kt @@ -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,117 +695,227 @@ fun AutoScrollControls( }, label = "AutoScrollUnified" ) { 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( onClick = { onCollapseChange(false) }, - modifier = Modifier.size(40.dp) + modifier = Modifier.size(36.dp) ) { Icon( imageVector = Icons.Default.ChevronLeft, contentDescription = "Expand", - tint = MaterialTheme.colorScheme.onSurface - ) - } - - FilledIconButton( - onClick = onPlayPauseToggle, - modifier = Modifier.size(40.dp), - colors = IconButtonDefaults.filledIconButtonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary - ) - ) { - Icon( - imageVector = if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow, - contentDescription = if (isPlaying) "Pause" else "Play", - modifier = Modifier.size(24.dp) - ) - } - - } else { - IconButton( - onClick = onClose, - modifier = Modifier.size(40.dp) - ) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = "Close", - tint = MaterialTheme.colorScheme.error, - modifier = Modifier.size(20.dp) - ) - } - - Box( - modifier = Modifier - .width(1.dp) - .height(24.dp) - .background(MaterialTheme.colorScheme.outlineVariant) - ) - - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(0.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) - ) - - FilledIconButton( - onClick = onPlayPauseToggle, - modifier = Modifier.size(40.dp), - colors = IconButtonDefaults.filledIconButtonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary - ) - ) { - Icon( - imageVector = if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow, - contentDescription = if (isPlaying) "Pause" else "Play", - 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 ) } + + // Play/Pause Mini + Box(modifier = Modifier.size(36.dp), contentAlignment = Alignment.Center) { + FilledIconButton( + onClick = onPlayPauseToggle, + modifier = Modifier.size(36.dp), + colors = IconButtonDefaults.filledIconButtonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary + ) + ) { + Icon( + imageVector = if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow, + contentDescription = if (isPlaying) "Pause" else "Play", + 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(32.dp) + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "Close", + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.size(18.dp) + ) + } + } + } + + Spacer(Modifier.height(16.dp)) + + // Bottom Row: Primary Controls + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + // 1. Play/Pause Button + Box(modifier = Modifier.size(48.dp), contentAlignment = Alignment.Center) { + FilledIconButton( + onClick = onPlayPauseToggle, + modifier = Modifier.size(48.dp), + colors = IconButtonDefaults.filledIconButtonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary + ) + ) { + Icon( + imageVector = if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow, + contentDescription = if (isPlaying) "Pause" else "Play", + 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( + onClick = { onSpeedChange((speed + 0.1f).coerceAtMost(10f)) }, + 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 9e38a16..0935b0b 100644 --- a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderScreen.kt +++ b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderScreen.kt @@ -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(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) + } ) } diff --git a/app/src/main/java/com/aryan/reader/pdf/PdfVerticalReader.kt b/app/src/main/java/com/aryan/reader/pdf/PdfVerticalReader.kt index 8177253..986726b 100644 --- a/app/src/main/java/com/aryan/reader/pdf/PdfVerticalReader.kt +++ b/app/src/main/java/com/aryan/reader/pdf/PdfVerticalReader.kt @@ -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) { 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 bd44a7a..d4b0f27 100644 --- a/app/src/main/java/com/aryan/reader/pdf/PdfViewerScreen.kt +++ b/app/src/main/java/com/aryan/reader/pdf/PdfViewerScreen.kt @@ -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(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) + } ) } }