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:
parent
579b6f25bf
commit
d16bdd70d4
67 changed files with 9724 additions and 1890 deletions
|
|
@ -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(
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue