General improvements (#151)
* Updated folder synchronization logic and redesigned the file type filter dialog. * Fixed CFI generation and navigation stability in the EPUB reader. * Improved CFI scrolling and position calculation in the EPUB reader by implementing text node traversal using `TreeWalker`. This ensures accurate positioning and scrolling when a CFI offset spans multiple fragmented text nodes. * fix fb2 multiline titles, retain footnotes, and prevent stream leaks - Fix FB2 titles with multiple paragraphs by inserting breaks/spaces - Prevent resource leaks by properly closing InputStreams in all importers - Retain "notes" and "comments" sections in FB2 instead of skipping them - Add support for FB2 poem, stanza, cite, and link tags * Implement persistence for zoom and pan states when pan lock is enabled in the PDF reader. * Added `FileTypeBadge` to home and library screens * Bump version to 1.0.41(42)
This commit is contained in:
parent
65e0570d0e
commit
26692d2c05
14 changed files with 747 additions and 664 deletions
|
|
@ -432,6 +432,7 @@ internal fun PdfPageComposable(
|
|||
draggingBoxId: String? = null,
|
||||
isScrollLocked: Boolean = false,
|
||||
isVisible: Boolean = true,
|
||||
isActivePage: Boolean = true,
|
||||
isStylusOnlyMode: Boolean = false,
|
||||
isHighlighterSnapEnabled: Boolean = false,
|
||||
userHighlights: List<PdfUserHighlight> = emptyList(),
|
||||
|
|
@ -442,7 +443,9 @@ internal fun PdfPageComposable(
|
|||
onTts: (Int, Int) -> Unit = { _, _ -> },
|
||||
activeToolThickness: Float = 0f,
|
||||
customHighlightColors: Map<PdfHighlightColor, Color> = emptyMap(),
|
||||
onPaletteClick: (() -> Unit)? = null
|
||||
onPaletteClick: (() -> Unit)? = null,
|
||||
lockedState: Triple<Float, Float, Float>? = null,
|
||||
onZoomAndPanChanged: ((Float, Offset) -> Unit)? = null
|
||||
) {
|
||||
val pdfDocumentItem = pdfDocument.item
|
||||
var bitmapState by remember { mutableStateOf(PdfThumbnailCache.get(pageIndex)) }
|
||||
|
|
@ -474,6 +477,10 @@ internal fun PdfPageComposable(
|
|||
var scale by remember { mutableFloatStateOf(1f) }
|
||||
var offset by remember { mutableStateOf(Offset.Zero) }
|
||||
|
||||
LaunchedEffect(scale, offset) {
|
||||
onZoomAndPanChanged?.invoke(scale, offset)
|
||||
}
|
||||
|
||||
val currentOnSingleTap by rememberUpdatedState(onSingleTap)
|
||||
val currentOnDoubleTap by rememberUpdatedState(onDoubleTap)
|
||||
|
||||
|
|
@ -720,12 +727,6 @@ internal fun PdfPageComposable(
|
|||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(pageIndex) {
|
||||
scale = 1f
|
||||
offset = Offset.Zero
|
||||
onScaleChanged(1f)
|
||||
}
|
||||
|
||||
LaunchedEffect(isPerformingOcrForSelection) { onOcrStateChange(isPerformingOcrForSelection) }
|
||||
|
||||
LaunchedEffect(
|
||||
|
|
@ -1067,9 +1068,10 @@ internal fun PdfPageComposable(
|
|||
canvasHeightPx.floatValue,
|
||||
isVerticalScroll,
|
||||
isScrolling,
|
||||
virtualPage
|
||||
virtualPage,
|
||||
isActivePage
|
||||
) {
|
||||
val needsTiling = effectiveScale > 1f || actualBitmapWidthPx > 3000 || actualBitmapHeightPx > 3000
|
||||
val needsTiling = (effectiveScale > 1f || actualBitmapWidthPx > 3000 || actualBitmapHeightPx > 3000) && (isVerticalScroll || isActivePage)
|
||||
if (!needsTiling) {
|
||||
if (tiles.isNotEmpty()) {
|
||||
val oldTiles = tiles
|
||||
|
|
@ -2594,7 +2596,7 @@ internal fun PdfPageComposable(
|
|||
}
|
||||
}
|
||||
}, onDoubleTap = { tapOffset ->
|
||||
if (isZoomEnabled && !isVerticalScroll) {
|
||||
if (isZoomEnabled && !isVerticalScroll && !isScrollLocked) {
|
||||
if (actualBitmapWidthPx == 0) return@detectTapGestures
|
||||
coroutineScope.launch {
|
||||
val startScale = scale
|
||||
|
|
@ -2689,7 +2691,11 @@ internal fun PdfPageComposable(
|
|||
|
||||
if (!canceled) {
|
||||
val rawPanChange = event.calculatePan()
|
||||
val panChange = if (isScrollLocked) Offset(0f, rawPanChange.y) else rawPanChange
|
||||
val panChange = if (isScrollLocked && pointerCount == 1) {
|
||||
if (isVerticalScroll) Offset(0f, rawPanChange.y) else Offset.Zero
|
||||
} else {
|
||||
rawPanChange
|
||||
}
|
||||
val zoomChange = event.calculateZoom()
|
||||
|
||||
if (scale > 1f) {
|
||||
|
|
@ -3112,14 +3118,21 @@ internal fun PdfPageComposable(
|
|||
}
|
||||
|
||||
LaunchedEffect(
|
||||
this@BoxWithConstraints.maxWidth, this@BoxWithConstraints.maxHeight
|
||||
pageIndex, this@BoxWithConstraints.maxWidth, this@BoxWithConstraints.maxHeight,
|
||||
isScrollLocked, lockedState
|
||||
) {
|
||||
scale = 1f
|
||||
offset = Offset.Zero
|
||||
onScaleChanged(1f)
|
||||
if (isScrollLocked && !isVerticalScroll && lockedState != null) {
|
||||
scale = lockedState.first
|
||||
offset = Offset(lockedState.second, lockedState.third)
|
||||
onScaleChanged(scale)
|
||||
} else if (!isScrollLocked && !isVerticalScroll) {
|
||||
scale = 1f
|
||||
offset = Offset.Zero
|
||||
onScaleChanged(1f)
|
||||
}
|
||||
|
||||
Timber.d(
|
||||
"PdfPageComposable Page $pageIndex | Constraints: maxWidth=${this@BoxWithConstraints.maxWidth}, maxHeight=${this@BoxWithConstraints.maxHeight}"
|
||||
"PdfPageComposable Page $pageIndex initialized/resized/locked. scale=$scale, offset=$offset"
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -234,7 +234,9 @@ internal fun PdfVerticalReader(
|
|||
onTts: (Int, Int) -> Unit = { _, _ -> },
|
||||
activeToolThickness: Float = 0f,
|
||||
customHighlightColors: Map<PdfHighlightColor, Color> = emptyMap(),
|
||||
onPaletteClick: () -> Unit = {}
|
||||
onPaletteClick: () -> Unit = {},
|
||||
lockedState: Triple<Float, Float, Float>? = null,
|
||||
onZoomAndPanChanged: ((Float, Offset) -> Unit)? = null
|
||||
) {
|
||||
SideEffect { Timber.tag("PdfDrawPerf").v("LIST: PdfVerticalReader Recomposing.") }
|
||||
DisposableEffect(state) {
|
||||
|
|
@ -336,6 +338,10 @@ internal fun PdfVerticalReader(
|
|||
val panXAnimatable = remember { Animatable(if ((screenWidth * fitZoom) < screenWidth) (screenWidth - (screenWidth * fitZoom)) / 2f else 0f) }
|
||||
val panYAnimatable = remember { Animatable(0f) }
|
||||
|
||||
LaunchedEffect(zoomAnimatable.value, panXAnimatable.value, panYAnimatable.value) {
|
||||
onZoomAndPanChanged?.invoke(zoomAnimatable.value, Offset(panXAnimatable.value, panYAnimatable.value))
|
||||
}
|
||||
|
||||
var isResizing by remember { mutableStateOf(false) }
|
||||
var previousScreenWidth by remember { mutableFloatStateOf(0f) }
|
||||
var previousScreenHeight by remember { mutableFloatStateOf(0f) }
|
||||
|
|
@ -401,6 +407,12 @@ internal fun PdfVerticalReader(
|
|||
delay(50)
|
||||
isResizing = false
|
||||
targetPageDuringResize.intValue = -1
|
||||
} else if (isScrollLocked && lockedState != null) {
|
||||
val (savedScale, savedPanX, _) = lockedState
|
||||
coroutineScope {
|
||||
launch { zoomAnimatable.snapTo(savedScale) }
|
||||
launch { panXAnimatable.snapTo(savedPanX) }
|
||||
}
|
||||
}
|
||||
isInitialLayout = false
|
||||
}
|
||||
|
|
@ -768,63 +780,65 @@ internal fun PdfVerticalReader(
|
|||
}
|
||||
|
||||
val onDoubleTapToZoom: (Offset) -> Unit = { tapScreenOffset ->
|
||||
val currentZoom = zoomAnimatable.value
|
||||
if (!isScrollLocked) {
|
||||
val currentZoom = zoomAnimatable.value
|
||||
|
||||
val targetZoom = when {
|
||||
currentZoom < 0.95f -> 1f
|
||||
currentZoom < 2.45f -> 2.5f
|
||||
else -> fitZoom
|
||||
}
|
||||
|
||||
val startPanX = panXAnimatable.value
|
||||
val startPanY = panYAnimatable.value
|
||||
|
||||
scope.launch {
|
||||
zoomAnimatable.stop()
|
||||
panXAnimatable.stop()
|
||||
panYAnimatable.stop()
|
||||
|
||||
val pivotContentX = (tapScreenOffset.x - startPanX) / currentZoom
|
||||
val pivotContentY = (tapScreenOffset.y - startPanY) / currentZoom
|
||||
|
||||
val rawNextPanX = tapScreenOffset.x - (pivotContentX * targetZoom)
|
||||
val rawNextPanY = tapScreenOffset.y - (pivotContentY * targetZoom)
|
||||
|
||||
val (finalZoom, finalX, finalY) = clampCamera(targetZoom, 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)) }
|
||||
val targetZoom = when {
|
||||
currentZoom < 0.95f -> 1f
|
||||
currentZoom < 2.45f -> 2.5f
|
||||
else -> fitZoom
|
||||
}
|
||||
|
||||
onZoomChange(zoomAnimatable.value)
|
||||
val startPanX = panXAnimatable.value
|
||||
val startPanY = panYAnimatable.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
|
||||
scope.launch {
|
||||
zoomAnimatable.stop()
|
||||
panXAnimatable.stop()
|
||||
panYAnimatable.stop()
|
||||
|
||||
val pivotContentX = (tapScreenOffset.x - startPanX) / currentZoom
|
||||
val pivotContentY = (tapScreenOffset.y - startPanY) / currentZoom
|
||||
|
||||
val rawNextPanX = tapScreenOffset.x - (pivotContentX * targetZoom)
|
||||
val rawNextPanY = tapScreenOffset.y - (pivotContentY * targetZoom)
|
||||
|
||||
val (finalZoom, finalX, finalY) = clampCamera(targetZoom, 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)
|
||||
}
|
||||
panXAnimatable.updateBounds(lowerBound = finalMinX, upperBound = finalMaxX)
|
||||
|
||||
val zDocH = totalDocHeight * finalZoom
|
||||
val minScrollY = (screenHeight - footerHeightPx - zDocH).coerceAtMost(headerHeightPx)
|
||||
panYAnimatable.updateBounds(lowerBound = minScrollY, upperBound = headerHeightPx)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1042,7 +1056,7 @@ internal fun PdfVerticalReader(
|
|||
|
||||
val zoomChange = event.calculateZoom()
|
||||
val rawPanChange = event.calculatePan()
|
||||
val panChange = if (isScrollLocked) Offset(0f, rawPanChange.y) else rawPanChange
|
||||
val panChange = if (isScrollLocked && !isMultiTouch) Offset(0f, rawPanChange.y) else rawPanChange
|
||||
|
||||
val centroid = event.calculateCentroid(useCurrent = false)
|
||||
val panMagnitude = panChange.getDistance()
|
||||
|
|
|
|||
|
|
@ -583,6 +583,25 @@ private fun getSuggestedFilename(originalName: String?, isAnnotated: Boolean): S
|
|||
return "${safeBase}${suffix}_${shortId}.pdf"
|
||||
}
|
||||
|
||||
private fun savePdfLockedState(context: Context, bookId: String, scale: Float, offsetX: Float, offsetY: Float) {
|
||||
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
|
||||
prefs.edit {
|
||||
putFloat("pdf_locked_scale_$bookId", scale)
|
||||
putFloat("pdf_locked_offset_x_$bookId", offsetX)
|
||||
putFloat("pdf_locked_offset_y_$bookId", offsetY)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadPdfLockedState(context: Context, bookId: String): Triple<Float, Float, Float>? {
|
||||
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
|
||||
if (!prefs.contains("pdf_locked_scale_$bookId")) return null
|
||||
return Triple(
|
||||
prefs.getFloat("pdf_locked_scale_$bookId", 1f),
|
||||
prefs.getFloat("pdf_locked_offset_x_$bookId", 0f),
|
||||
prefs.getFloat("pdf_locked_offset_y_$bookId", 0f)
|
||||
)
|
||||
}
|
||||
|
||||
private enum class SaveMode {
|
||||
ORIGINAL, ANNOTATED
|
||||
}
|
||||
|
|
@ -1181,6 +1200,9 @@ fun PdfViewerScreen(
|
|||
var documentPassword by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
var pendingRestorePage by rememberSaveable { mutableStateOf(initialPage) }
|
||||
var isScrollLocked by remember { mutableStateOf(false) }
|
||||
var lockedState by remember { mutableStateOf<Triple<Float, Float, Float>?>(null) }
|
||||
var currentActiveScale by remember { mutableFloatStateOf(1f) }
|
||||
var currentActiveOffset by remember { mutableStateOf(Offset.Zero) }
|
||||
var showPasswordDialog by remember { mutableStateOf(false) }
|
||||
var isPasswordError by remember { mutableStateOf(false) }
|
||||
LocalView.current
|
||||
|
|
@ -1239,6 +1261,7 @@ fun PdfViewerScreen(
|
|||
LaunchedEffect(bookId) {
|
||||
isScrollLocked = loadPdfScrollLocked(context, bookId)
|
||||
isFullScreen = loadPdfFullScreen(context, bookId)
|
||||
lockedState = loadPdfLockedState(context, bookId)
|
||||
}
|
||||
|
||||
var isAutoScrollModeActive by remember { mutableStateOf(false) }
|
||||
|
|
@ -1504,6 +1527,14 @@ fun PdfViewerScreen(
|
|||
|
||||
LaunchedEffect(displayMode) { saveDisplayMode(context, displayMode) }
|
||||
|
||||
LaunchedEffect(currentActiveScale, currentActiveOffset, isScrollLocked) {
|
||||
if (isScrollLocked) {
|
||||
delay(500)
|
||||
lockedState = Triple(currentActiveScale, currentActiveOffset.x, currentActiveOffset.y)
|
||||
savePdfLockedState(context, bookId, currentActiveScale, currentActiveOffset.x, currentActiveOffset.y)
|
||||
}
|
||||
}
|
||||
|
||||
val annotationSettingsRepo = remember(context) { AnnotationSettingsRepository(context) }
|
||||
val toolSettings by annotationSettingsRepo.settings.collectAsState()
|
||||
var showToolSettings by rememberSaveable { mutableStateOf(false) }
|
||||
|
|
@ -4367,7 +4398,7 @@ fun PdfViewerScreen(
|
|||
key = { it },
|
||||
beyondViewportPageCount = dynamicBeyondViewportPageCount,
|
||||
userScrollEnabled = run {
|
||||
val enabled = currentPageScale == 1f && !(ttsState.isPlaying || ttsState.isLoading || searchState.isSearchActive) && !isPageSliderVisible && paginationDraggingBoxId == null
|
||||
val enabled = (currentPageScale == 1f || (isScrollLocked && displayMode == DisplayMode.PAGINATION)) && !(ttsState.isPlaying || ttsState.isLoading || searchState.isSearchActive) && !isPageSliderVisible && paginationDraggingBoxId == null
|
||||
SideEffect {
|
||||
Timber.tag("PdfZoomDebug").v("Pager Scroll Enabled: $enabled (Scale: $currentPageScale, Playing: ${ttsState.isPlaying}, Slider: $isPageSliderVisible, DraggingBox: $paginationDraggingBoxId)")
|
||||
}
|
||||
|
|
@ -4619,6 +4650,13 @@ fun PdfViewerScreen(
|
|||
onNoteRequested = onNoteRequested,
|
||||
onTts = { pageIdx, charIdx -> startTtsWithPermissionCheck(pageIdx, charIdx) },
|
||||
activeToolThickness = currentStrokeWidthState,
|
||||
lockedState = lockedState,
|
||||
onZoomAndPanChanged = { newScale, newOffset ->
|
||||
if (pagerState.currentPage == pageIndex) {
|
||||
currentActiveScale = newScale
|
||||
currentActiveOffset = newOffset
|
||||
}
|
||||
},
|
||||
onTwoFingerSwipe = { direction ->
|
||||
coroutineScope.launch {
|
||||
val targetPage =
|
||||
|
|
@ -4774,6 +4812,8 @@ fun PdfViewerScreen(
|
|||
},
|
||||
onDragPageTurn = { /* Handled in onTextBoxDrag */ },
|
||||
isVisible = isVisiblePage,
|
||||
isActivePage = pagerState.currentPage == pageIndex,
|
||||
isScrolling = pagerState.isScrollInProgress
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -5025,7 +5065,12 @@ fun PdfViewerScreen(
|
|||
isAutoScrollPlaying = isAutoScrollPlaying,
|
||||
isAutoScrollTempPaused = isAutoScrollTempPaused,
|
||||
autoScrollSpeed = autoScrollSpeed * 0.5f,
|
||||
onInteractionListener = onAutoScrollInteraction
|
||||
onInteractionListener = onAutoScrollInteraction,
|
||||
lockedState = lockedState,
|
||||
onZoomAndPanChanged = { newScale, newOffset ->
|
||||
currentActiveScale = newScale
|
||||
currentActiveOffset = newOffset
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -5529,6 +5574,10 @@ fun PdfViewerScreen(
|
|||
onClick = {
|
||||
isScrollLocked = !isScrollLocked
|
||||
savePdfScrollLocked(context, bookId, isScrollLocked)
|
||||
if (isScrollLocked) {
|
||||
savePdfLockedState(context, bookId, currentActiveScale, currentActiveOffset.x, currentActiveOffset.y)
|
||||
lockedState = Triple(currentActiveScale, currentActiveOffset.x, currentActiveOffset.y)
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = if (isScrollLocked) Icons.Default.Lock else Icons.Default.LockOpen,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue