V1.0.46 oss (#253)

* add a UI trigger for demo annotations

* Implement internal link navigation

* Simplify bottom padding logic in `EpubReaderScreen` for vertical scroll mode

* Add support for Calibre metadata

* Implement advanced library management with Room-backed collections, tags, and smart rules.

* Display book series information in book details and remove tags from home screen

* Implement zoom reset functionality in PDF viewer

* Add a "Pages" thumbnail view to the PDF navigation drawer

* Implement jump-back navigation in PDF viewer

* MainViewModel: disable FolderSyncWorker, add log export, and enhance PDF position logging

* Replace Gson with Kotlin Serialization in SmartCollectionEngine

* Implement ONNX-based speech bubble detection

* Implement speech bubble detection in PDF viewer

* Implement "Smart Comic Zoom" feature for PDF manga/comic reading

* Implement reader session persistence and restoration in `MainViewModel`

* Add tap-to-turn page feature to PDF viewer

* Improve book cache management and recovery

* Refactor top overlay padding logic in `PdfViewerScreen`

* Implement stylus eraser support in PDF reader

* Refactor ReaderTextFormatPanel to use ModalBottomSheet and add new formatting controls

* Introduce a customizable horizontal margin setting for the EPUB reader

* Refine folder sync and metadata handling for better conflict resolution and stability.

* Introduce user-adjustable image scaling for the EPUB reader

* Use maxWidthPx as default width fallback for blocks

* Improve cross-page text selection and header styling in the paginated reader

* Refactor speech bubble detection and UI to support segmentation masks and interactive scaling

* Improve speech bubble detection masking and rendering quality

* Update ONNX speech bubble detector to use `.ort` model and optimize inference

* Improve pagination accuracy

* Implement hierarchical folder navigation for the library.

* Refactor library item layout for improved space efficiency

* Add support for browsing and downloading Google Fonts

* Persist library landing state and add shelf search functionality

* Fix base tts sample.

* Move SpeechBubbleDetector to main source set and update ONNX dependency

* Implement a "locate" feature and improve synchronization for TTS (Text-to-Speech) playback across EPUB and PDF readers.

* Refine TTS synchronization and voice management in the EPUB reader

* Adjust OCR checks for OSS flavor and optimize external dictionary intent flags

* Add "Locate" button to PDF drawer's pages tab and move the page number to bottom right of thumbnail

* fix various crashes

* Refactor SpeechBubbleDetector into product flavors

* Implement speech bubble detection caching and background prefetching

* Implement on-demand download for Bubble Zoom ML model

* Add smooth animation for PDF speech bubble expansion

* fixes #252

* hide Google Fonts option, in FontsScreen, for offline variant

* Bump version to 1.0.46(46)
This commit is contained in:
Aryan 2026-04-29 16:37:53 +05:30 committed by GitHub
parent 579b6f25bf
commit d16bdd70d4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
67 changed files with 9724 additions and 1890 deletions

View file

@ -23,6 +23,7 @@
package com.aryan.reader.pdf
import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.graphics.RectF
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
@ -90,6 +91,9 @@ import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.PointerType
import androidx.compose.ui.input.pointer.isPrimaryPressed
import androidx.compose.ui.input.pointer.isSecondaryPressed
import androidx.compose.ui.input.pointer.isTertiaryPressed
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.positionChanged
import androidx.compose.ui.input.pointer.util.VelocityTracker
@ -108,6 +112,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.zIndex
import com.aryan.reader.SearchResult
import com.aryan.reader.ml.SpeechBubble
import com.aryan.reader.pdf.data.PdfAnnotation
import com.aryan.reader.pdf.data.PdfTextBox
import com.aryan.reader.pdf.data.VirtualPage
@ -217,8 +222,8 @@ internal fun PdfVerticalReader(
isEditMode: Boolean = false,
allAnnotations: () -> Map<Int, List<PdfAnnotation>> = { emptyMap() },
drawingState: PdfDrawingState,
onDrawStart: (Int, PdfPoint) -> Unit,
onDraw: (Int, PdfPoint) -> Unit,
onDrawStart: (Int, PdfPoint, Boolean) -> Unit,
onDraw: (Int, PdfPoint, Boolean) -> Unit,
onDrawEnd: () -> Unit,
onOcrModelDownloading: () -> Unit = {},
selectedTool: InkType,
@ -247,7 +252,10 @@ internal fun PdfVerticalReader(
customHighlightColors: Map<PdfHighlightColor, Color> = emptyMap(),
onPaletteClick: () -> Unit = {},
lockedState: Triple<Float, Float, Float>? = null,
onZoomAndPanChanged: ((Float, Offset) -> Unit)? = null
onZoomAndPanChanged: ((Float, Offset) -> Unit)? = null,
resetZoomTrigger: Long = 0L,
isBubbleZoomModeActive: Boolean = false,
onDetectBubbles: suspend (Int, Bitmap) -> List<SpeechBubble> = { _, _ -> emptyList() }
) {
SideEffect { Timber.tag("PdfDrawPerf").v("LIST: PdfVerticalReader Recomposing.") }
DisposableEffect(state) {
@ -260,6 +268,7 @@ internal fun PdfVerticalReader(
}
}
var globalEraserPosition by remember { mutableStateOf<Offset?>(null) }
var isStylusEraserOverride by remember { mutableStateOf(false) }
val isDarkMode = activeTheme.isDark || activeTheme.id == "reverse"
BoxWithConstraints(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.TopStart) {
val imeInsets = WindowInsets.ime
@ -393,18 +402,17 @@ internal fun PdfVerticalReader(
val zoomedDocHeight = totalDocHeight * savedScale
val minPanY = (screenHeight - footerHeightPx - zoomedDocHeight).coerceAtMost(headerHeightPx)
val maxPanY = headerHeightPx
zoomAnimatable.stop()
panXAnimatable.stop()
panYAnimatable.stop()
panXAnimatable.updateBounds(minPanX, maxPanX)
panYAnimatable.updateBounds(minPanY, maxPanY)
panYAnimatable.updateBounds(minPanY, headerHeightPx)
zoomAnimatable.snapTo(savedScale)
panXAnimatable.snapTo(savedPanX)
panYAnimatable.snapTo(savedPanY.coerceIn(minPanY, maxPanY))
panYAnimatable.snapTo(savedPanY.coerceIn(minPanY, headerHeightPx))
Timber.tag("PdfLockDiagnostic").d("RESTORE SNAP COMPLETE: Scale=${zoomAnimatable.value}, X=${panXAnimatable.value}, Y=${panYAnimatable.value}")
@ -492,6 +500,65 @@ internal fun PdfVerticalReader(
return clampValues(targetZoom, targetPanX, targetPanY)
}
LaunchedEffect(resetZoomTrigger) {
if (resetZoomTrigger != 0L && zoomAnimatable.value > fitZoom && !isScrollLocked) {
scope.launch {
zoomAnimatable.stop()
panXAnimatable.stop()
panYAnimatable.stop()
val startZoom = zoomAnimatable.value
val startPanX = panXAnimatable.value
val startPanY = panYAnimatable.value
val pivotScreenX = screenWidth / 2f
val pivotScreenY = screenHeight / 2f
val pivotContentX = (pivotScreenX - startPanX) / startZoom
val pivotContentY = (pivotScreenY - startPanY) / startZoom
val rawNextPanX = pivotScreenX - (pivotContentX * fitZoom)
val rawNextPanY = pivotScreenY - (pivotContentY * fitZoom)
val (finalZoom, finalX, finalY) = clampCamera(fitZoom, rawNextPanX, rawNextPanY)
panXAnimatable.updateBounds(
lowerBound = minOf(panXAnimatable.lowerBound ?: finalX, finalX, startPanX),
upperBound = maxOf(panXAnimatable.upperBound ?: finalX, finalX, startPanX)
)
panYAnimatable.updateBounds(
lowerBound = minOf(panYAnimatable.lowerBound ?: finalY, finalY, startPanY),
upperBound = maxOf(panYAnimatable.upperBound ?: finalY, finalY, startPanY)
)
coroutineScope {
launch { zoomAnimatable.animateTo(finalZoom, animationSpec = tween(400, easing = FastOutSlowInEasing)) }
launch { panXAnimatable.animateTo(finalX, animationSpec = tween(400, easing = FastOutSlowInEasing)) }
launch { panYAnimatable.animateTo(finalY, animationSpec = tween(400, easing = FastOutSlowInEasing)) }
}
onZoomChange(zoomAnimatable.value)
val zoomedDocWidth = screenWidth * finalZoom
val finalMinX: Float
val finalMaxX: Float
if (zoomedDocWidth < screenWidth) {
val centeredX = (screenWidth - zoomedDocWidth) / 2f
finalMinX = centeredX
finalMaxX = centeredX
} else {
finalMinX = -(zoomedDocWidth - screenWidth)
finalMaxX = 0f
}
panXAnimatable.updateBounds(lowerBound = finalMinX, upperBound = finalMaxX)
val zDocH = totalDocHeight * finalZoom
val minScrollY = (screenHeight - footerHeightPx - zDocH).coerceAtMost(headerHeightPx)
panYAnimatable.updateBounds(lowerBound = minScrollY, upperBound = headerHeightPx)
}
}
}
LaunchedEffect(
totalDocHeight, screenHeight, headerHeightPx, footerHeightPx, zoomAnimatable.value, isInteracting, isFlinging, isResizing
) {
@ -920,6 +987,15 @@ internal fun PdfVerticalReader(
return@awaitEachGesture
}
val buttons = currentEvent.buttons
Timber.tag("StylusEraserDiagnostic").d(
"VerticalReader | Type: ${down.type} | isPrimary: ${buttons.isPrimaryPressed} | isSecondary: ${buttons.isSecondaryPressed} | isTertiary: ${buttons.isTertiaryPressed} | buttonsString: $buttons"
)
val isEraserOverride = down.type == PointerType.Eraser ||
(down.type == PointerType.Stylus && currentEvent.buttons.isSecondaryPressed)
isStylusEraserOverride = isEraserOverride
fun getPageAndPoint(screenOffset: Offset): Pair<Int, PdfPoint>? {
val zoom = zoomAnimatable.value
val panX = panXAnimatable.value
@ -943,14 +1019,14 @@ internal fun PdfVerticalReader(
var isCanceled = false
try {
if (selectedTool == InkType.ERASER) {
if (selectedTool == InkType.ERASER || isEraserOverride) {
globalEraserPosition = down.position
}
val startData = getPageAndPoint(down.position)
if (startData != null) {
val (pageIndex, point) = startData
onDrawStart(pageIndex, point)
onDrawStart(pageIndex, point, isEraserOverride)
down.consume()
}
@ -969,7 +1045,7 @@ internal fun PdfVerticalReader(
if (change == null || !change.pressed) break
if (change.positionChanged()) {
if (selectedTool == InkType.ERASER) {
if (selectedTool == InkType.ERASER || isEraserOverride) {
globalEraserPosition = change.position
}
@ -977,11 +1053,11 @@ internal fun PdfVerticalReader(
if (dragData != null) {
val (pageIndex, point) = dragData
if (pageIndex != lastPageIndex && selectedTool != InkType.ERASER) {
if (pageIndex != lastPageIndex && selectedTool != InkType.ERASER && !isEraserOverride) {
onDrawEnd()
onDrawStart(pageIndex, point)
onDrawStart(pageIndex, point, isEraserOverride)
} else {
onDraw(pageIndex, point)
onDraw(pageIndex, point, isEraserOverride)
}
lastPageIndex = pageIndex
}
@ -993,6 +1069,7 @@ internal fun PdfVerticalReader(
onDrawEnd()
}
globalEraserPosition = null
isStylusEraserOverride = false
}
}
}
@ -1472,16 +1549,20 @@ internal fun PdfVerticalReader(
}
val onDrawStartLambda = remember(page.index, onDrawStart) {
{ point: PdfPoint -> onDrawStart(page.index, point) }
{ point: PdfPoint, isEraserOverride: Boolean ->
onDrawStart(page.index, point, isEraserOverride)
}
}
val currentOnDraw by rememberUpdatedState(onDraw)
val onDrawLambda = remember(page.index) {
{ point: PdfPoint -> currentOnDraw(page.index, point) }
{ point: PdfPoint, isEraserOverride: Boolean ->
currentOnDraw(page.index, point, isEraserOverride)
}
}
val onSingleTapLambda = remember(onPageClick) {
{
{ _: Offset? ->
selectionClearTrigger++
onPageClick()
}
@ -1759,7 +1840,9 @@ internal fun PdfVerticalReader(
draggingBoxId = null
}
},
draggingBoxId = draggingBoxId
draggingBoxId = draggingBoxId,
isBubbleZoomModeActive = isBubbleZoomModeActive,
onDetectBubbles = onDetectBubbles
)
}
@ -1875,6 +1958,7 @@ internal fun PdfVerticalReader(
animationSpec = tween(durationMillis = 300),
label = "scrollbarAlpha"
)
val safeCurrentPage = if (totalPages > 0) state.currentPage.coerceIn(0, totalPages - 1) else 0
val samsungBlue = Color(0xFF4285F4)
val samsungBlueDark = Color(0xFF1976D2)
@ -1934,7 +2018,7 @@ internal fun PdfVerticalReader(
.alpha(scrollbarAlpha)) {
Row(verticalAlignment = Alignment.CenterVertically) {
AnimatedVisibility(
visible = isDraggingScrollbar,
visible = isDraggingScrollbar && totalPages > 0,
enter = fadeIn() + androidx.compose.animation.slideInHorizontally {
it / 2
},
@ -1948,7 +2032,7 @@ internal fun PdfVerticalReader(
modifier = Modifier.padding(end = 12.dp)
) {
Text(
text = "${state.currentPage + 1}/${totalPages}",
text = "${safeCurrentPage + 1}/$totalPages",
style = MaterialTheme.typography.titleMedium.copy(
fontSize = 16.sp, fontWeight = FontWeight.Bold
),
@ -2060,7 +2144,7 @@ internal fun PdfVerticalReader(
}
}
if (isEditMode && selectedTool == InkType.ERASER && globalEraserPosition != null) {
if (isEditMode && (selectedTool == InkType.ERASER || isStylusEraserOverride) && globalEraserPosition != null) {
Canvas(modifier = Modifier.fillMaxSize()) {
val pos = globalEraserPosition!!
val radiusPx = if (activeToolThickness > 0f) {
@ -2127,4 +2211,4 @@ internal fun PdfVerticalReader(
}
}
}
}
}