v1.0.47 (#279)
* Add performance and stylus debugging logs * Refactor and decouple UI models from `MainViewModel` * Refactor library state management and projection logic * Implement desktop shell using Compose Multiplatform * Implement desktop shell using Compose Multiplatform * Implement desktop shell using Compose Multiplatform * Introduce ReaderEngine and enhance EPUB reader features in windows app * Move core paginated reader logic to a Kotlin Multiplatform `shared` module and introduce experimental desktop support. * Implement PDF rendering and text extraction for desktop using Pdfium * Add `NonReaderScreens.kt` and UI dependencies * Refactor and centralize library state management and models to improve cross-platform consistency * Implement JSON persistence for desktop library and enhance library management features including shelf CRUD, tagging, and metadata editing * Implement PDF annotation system and enhanced zoom controls for the desktop viewer * Implement WebView-based EPUB rendering for desktop using CEF and embedded resources * Optimize UI state projection, navigation state handling, and main screen pager performance * Implement Bring Your Own Key (BYOK) support for AI features in OSS version * Support Gemini-based Cloud TTS with BYOK support for OSS builds * Refactor table cell image sizing in `PaginatedReader` and improve `MobiParser` native library loading and error handling. * crash fixes * Enhance navigation stability with lifecycle-aware safety checks and update `navigation-compose` to 2.9.6 * Implement dynamic bottom padding for the page info bar to account for device rounded corners * Implement bidirectional jump history navigation and replace the jump-back pill with a dedicated `PdfJumpHistoryBar` * Optimize PDF tiling performance and refine pan-and-fling gesture handling * Implement customizable toolbars with drag-and-drop reordering and placement for PDF and EPUB readers * Updated UI for customize toolbar * Refine drag-and-drop reordering and section assignment for PDF and EPUB reader controls * restructure PDF viewer UI component hierarchy to fix verifier crash * Implement separate text dimming factors for light and dark themes * Synchronize Pdfium access and improve resource lifecycle safety across Kotlin and native layers * Enhance image alignment in paginated and EPUB readers through anchor detection and style-based positioning * Centralize file type resolution logic and implement HTML sanitization during import * Introduce vertical margin customization and configurable progress bar positioning * texture support in epub reader * Enhance TTS session management, progress tracking, and diagnostic logging * Optimize library state projection and folder synchronization performance by refactoring collection lookups and refining metadata extraction logic. * Refine TTS page mapping for PDF and overhaul TTS control UI * Implement natural session completion logic in `TtsPlaybackManager` for cloud tts * Replace Snackbar with `CustomTopBanner` for notifications in `PdfViewerScreen` * Refine TTS playback continuity across PDF pages and improve state management for session transitions * Implement global texture transparency and enhance textured theme support across PDF and EPUB readers. * Update reader themes and improve texture rendering in page animations, EPUB UI, and immersive mode * Add Support Project screen * Optimize library performance via projection caching, batch database updates, and scoped folder synchronization. * Enhance folder synchronization with fallback query mechanisms and refactor annotation sidecar importing logic * Bump version to 1.0.47 (51)
This commit is contained in:
parent
f42de6b462
commit
d7a9cae9e1
126 changed files with 15287 additions and 3154 deletions
|
|
@ -40,6 +40,7 @@ import androidx.compose.ui.unit.Dp
|
|||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
|
|
@ -68,6 +69,9 @@ fun MagnifierComposable(
|
|||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
val magnifierWidthPx = size.width
|
||||
val magnifierHeightPx = size.height
|
||||
if (magnifierWidthPx <= 0f || magnifierHeightPx <= 0f || zoomFactor <= 0f) {
|
||||
return@Canvas
|
||||
}
|
||||
|
||||
Timber.d("Magnifier: START. scale=$currentScale, centerOnBitmap=$magnifierCenterOnBitmap")
|
||||
|
||||
|
|
@ -109,8 +113,10 @@ fun MagnifierComposable(
|
|||
val srcTop = (centerInTileBitmap.y - sourceRectHeight / 2f)
|
||||
Timber.d("Magnifier: Calculated source top-left=($srcLeft, $srcTop)")
|
||||
|
||||
val clampedSrcLeft = srcLeft.coerceIn(0f, bitmapToUse.width.toFloat() - sourceRectWidth.coerceAtLeast(1f))
|
||||
val clampedSrcTop = srcTop.coerceIn(0f, bitmapToUse.height.toFloat() - sourceRectHeight.coerceAtLeast(1f))
|
||||
val maxSrcLeft = max(0f, bitmapToUse.width.toFloat() - sourceRectWidth.coerceAtLeast(1f))
|
||||
val maxSrcTop = max(0f, bitmapToUse.height.toFloat() - sourceRectHeight.coerceAtLeast(1f))
|
||||
val clampedSrcLeft = srcLeft.coerceIn(0f, maxSrcLeft)
|
||||
val clampedSrcTop = srcTop.coerceIn(0f, maxSrcTop)
|
||||
Timber.d("Magnifier: Clamped source top-left=($clampedSrcLeft, $clampedSrcTop)")
|
||||
|
||||
val finalSrcLeftInt = clampedSrcLeft.roundToInt()
|
||||
|
|
@ -174,8 +180,10 @@ fun MagnifierComposable(
|
|||
val srcTop = (magnifierCenterOnBitmap.y - sourceRectHeight / 2f)
|
||||
Timber.d("Magnifier: Calculated source top-left=($srcLeft, $srcTop)")
|
||||
|
||||
val clampedSrcLeft = srcLeft.coerceIn(0f, sourceBitmap.width.toFloat() - sourceRectWidth.coerceAtLeast(1f))
|
||||
val clampedSrcTop = srcTop.coerceIn(0f, sourceBitmap.height.toFloat() - sourceRectHeight.coerceAtLeast(1f))
|
||||
val maxSrcLeft = max(0f, sourceBitmap.width.toFloat() - sourceRectWidth.coerceAtLeast(1f))
|
||||
val maxSrcTop = max(0f, sourceBitmap.height.toFloat() - sourceRectHeight.coerceAtLeast(1f))
|
||||
val clampedSrcLeft = srcLeft.coerceIn(0f, maxSrcLeft)
|
||||
val clampedSrcTop = srcTop.coerceIn(0f, maxSrcTop)
|
||||
Timber.d("Magnifier: Clamped source top-left=($clampedSrcLeft, $clampedSrcTop)")
|
||||
|
||||
val finalSrcLeftInt = clampedSrcLeft.roundToInt()
|
||||
|
|
@ -226,4 +234,4 @@ fun MagnifierComposable(
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package com.aryan.reader.pdf
|
||||
|
||||
import com.aryan.reader.shared.pdf.PdfiumAnnotationSubtype
|
||||
|
||||
object NativePdfiumBridge {
|
||||
init {
|
||||
System.loadLibrary("native-lib")
|
||||
|
|
@ -31,9 +33,9 @@ object NativePdfiumBridge {
|
|||
@JvmStatic external fun getAnnotRectAtPoint(pagePtr: Long, x: Double, y: Double): FloatArray?
|
||||
@JvmStatic external fun checkActionSupport(): Boolean
|
||||
|
||||
const val ANNOT_TEXT = 1 // Sticky Note
|
||||
const val ANNOT_LINK = 2 // Link
|
||||
const val ANNOT_HIGHLIGHT = 8 // Highlight
|
||||
const val ANNOT_INK = 12 // Freehand drawing
|
||||
const val ANNOT_WIDGET = 19
|
||||
}
|
||||
const val ANNOT_TEXT = PdfiumAnnotationSubtype.TEXT
|
||||
const val ANNOT_LINK = PdfiumAnnotationSubtype.LINK
|
||||
const val ANNOT_HIGHLIGHT = PdfiumAnnotationSubtype.HIGHLIGHT
|
||||
const val ANNOT_INK = PdfiumAnnotationSubtype.INK
|
||||
const val ANNOT_WIDGET = PdfiumAnnotationSubtype.WIDGET
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,7 +84,9 @@ fun VerticalScrollbar(
|
|||
|
||||
if (viewportRatio >= 1f) return@derivedStateOf null
|
||||
|
||||
val thumbHeight = (viewportHeight * viewportRatio).coerceIn(80f, viewportHeight / 2)
|
||||
val maxThumbHeight = viewportHeight / 2f
|
||||
val minThumbHeight = minOf(80f, maxThumbHeight)
|
||||
val thumbHeight = (viewportHeight * viewportRatio).coerceIn(minThumbHeight, maxThumbHeight)
|
||||
|
||||
val firstItemIndex = listState.firstVisibleItemIndex
|
||||
val firstItemOffset = listState.firstVisibleItemScrollOffset
|
||||
|
|
|
|||
|
|
@ -69,9 +69,13 @@ import androidx.compose.ui.graphics.BlendMode
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.ColorMatrix
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.ImageShader
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.ShaderBrush
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.StrokeJoin
|
||||
import androidx.compose.ui.graphics.TileMode
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.graphics.drawscope.clipRect
|
||||
|
|
@ -119,6 +123,7 @@ import androidx.core.graphics.scale
|
|||
import androidx.core.graphics.set
|
||||
import com.aryan.reader.R
|
||||
import com.aryan.reader.SearchResult
|
||||
import com.aryan.reader.loadReaderTextureBitmap
|
||||
import com.aryan.reader.ml.SpeechBubble
|
||||
import com.aryan.reader.pdf.data.PdfAnnotation
|
||||
import com.aryan.reader.pdf.data.PdfTextBox
|
||||
|
|
@ -127,6 +132,7 @@ import com.aryan.reader.pdf.ocr.OcrElement
|
|||
import com.aryan.reader.pdf.ocr.OcrResult
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
|
|
@ -180,6 +186,13 @@ data class PdfPoint(val x: Float, val y: Float, val timestamp: Long = 0L)
|
|||
|
||||
data class PdfTile(val bitmap: Bitmap, val renderRect: Rect, val tileId: Int, val renderScale: Float = 1f)
|
||||
|
||||
private const val PDF_TILE_SIZE_DP = 256
|
||||
private const val PDF_MAX_TILE_BITMAP_SIZE_PX = 3072
|
||||
private const val PDF_TILE_SCALE_TOLERANCE = 0.06f
|
||||
private const val PDF_TILE_IDLE_RENDER_DELAY_MS = 90L
|
||||
private const val PDF_PAGINATION_PAN_FLING_MIN_VELOCITY = 600f
|
||||
private const val PDF_PAGINATION_PAN_FLING_MULTIPLIER = 0.72f
|
||||
|
||||
enum class LinkSource {
|
||||
ANNOTATION, TEXT_CONTENT
|
||||
}
|
||||
|
|
@ -426,7 +439,10 @@ data class PageStaticData(
|
|||
val colorFilter: StableHolder<ColorFilter?>,
|
||||
val isDarkMode: Boolean,
|
||||
val excludeImages: Boolean,
|
||||
val imageRects: StableHolder<List<android.graphics.Rect>>
|
||||
val imageRects: StableHolder<List<android.graphics.Rect>>,
|
||||
val textureBitmap: StableHolder<ImageBitmap?>,
|
||||
val textureAlpha: Float,
|
||||
val textureBlendMode: BlendMode
|
||||
)
|
||||
|
||||
@Stable
|
||||
|
|
@ -497,6 +513,7 @@ internal fun PdfPageComposable(
|
|||
onTtsHighlightCenterCalculated: ((Float) -> Unit)? = null,
|
||||
onSearchHighlightCenterCalculated: ((Float) -> Unit)? = null,
|
||||
activeTheme: com.aryan.reader.ReaderTheme = com.aryan.reader.ReaderTheme("no_theme", "No Theme", Color.Unspecified, Color.Unspecified, false),
|
||||
activeTextureAlpha: Float = 0.55f,
|
||||
excludeImages: Boolean = false,
|
||||
onDoubleTap: ((Offset) -> Unit)? = null,
|
||||
isEditMode: Boolean = false,
|
||||
|
|
@ -552,7 +569,7 @@ internal fun PdfPageComposable(
|
|||
var isLoadingPage by remember { mutableStateOf(true) }
|
||||
var pageErrorMessage by remember { mutableStateOf<String?>(null) }
|
||||
val density = LocalDensity.current
|
||||
LocalContext.current
|
||||
val context = LocalContext.current
|
||||
val viewConfiguration = LocalViewConfiguration.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var isStylusEraserOverride by remember { mutableStateOf(false) }
|
||||
|
|
@ -564,6 +581,7 @@ internal fun PdfPageComposable(
|
|||
var isTransforming by remember { mutableStateOf(false) }
|
||||
var scale by remember { mutableFloatStateOf(1f) }
|
||||
var offset by remember { mutableStateOf(Offset.Zero) }
|
||||
var paginationPanFlingJob by remember { mutableStateOf<Job?>(null) }
|
||||
|
||||
LaunchedEffect(scale, offset) {
|
||||
onZoomAndPanChanged?.invoke(scale, offset)
|
||||
|
|
@ -590,8 +608,10 @@ internal fun PdfPageComposable(
|
|||
val pdfPageIndex = (virtualPage as? VirtualPage.PdfPage)?.pdfIndex ?: pageIndex
|
||||
|
||||
var tiles by remember { mutableStateOf<List<PdfTile>>(emptyList()) }
|
||||
val tileSizeDp = 256.dp
|
||||
val tileSizeDp = PDF_TILE_SIZE_DP.dp
|
||||
val tileSizePx = with(LocalDensity.current) { tileSizeDp.toPx().toInt() }
|
||||
val latestEffectiveScale by rememberUpdatedState(effectiveScale)
|
||||
val latestEffectiveOffset by rememberUpdatedState(effectiveOffset)
|
||||
|
||||
SideEffect {
|
||||
Timber.tag("PdfDrawPerf")
|
||||
|
|
@ -633,6 +653,8 @@ internal fun PdfPageComposable(
|
|||
var actualBitmapHeightPx by remember { mutableIntStateOf(0) }
|
||||
var currentPageRotation by remember { mutableIntStateOf(0) }
|
||||
|
||||
val needsTilingNow = (effectiveScale > 1f || actualBitmapWidthPx > 3000 || actualBitmapHeightPx > 3000) && (isVerticalScroll || isActivePage)
|
||||
|
||||
val canvasWidthPx = remember { mutableFloatStateOf(0f) }
|
||||
val canvasHeightPx = remember { mutableFloatStateOf(0f) }
|
||||
|
||||
|
|
@ -687,6 +709,15 @@ internal fun PdfPageComposable(
|
|||
activeTheme.backgroundColor
|
||||
}
|
||||
}
|
||||
val textureBitmap = remember(activeTheme.textureId) {
|
||||
loadReaderTextureBitmap(context, activeTheme.textureId)
|
||||
}
|
||||
val effectiveTextureAlpha = remember(activeTheme.textureId, activeTextureAlpha) {
|
||||
if (activeTheme.textureId == null) 0f else activeTextureAlpha.coerceIn(0f, 1f)
|
||||
}
|
||||
val textureBlendMode = remember(activeTheme.textureId, activeTheme.isDark, activeTheme.id) {
|
||||
if (activeTheme.isDark || activeTheme.id == "reverse") BlendMode.Screen else BlendMode.Multiply
|
||||
}
|
||||
|
||||
val centeringOffsetX by remember(canvasWidthPx.floatValue, actualBitmapWidthPx) {
|
||||
derivedStateOf { (canvasWidthPx.floatValue - actualBitmapWidthPx) / 2f }
|
||||
|
|
@ -1146,13 +1177,13 @@ internal fun PdfPageComposable(
|
|||
try {
|
||||
val pagePtr = pageWrapper.getNativePointer()
|
||||
if (pagePtr != 0L) {
|
||||
val objCount = NativePdfiumBridge.getPageObjectCount(pagePtr)
|
||||
val objCount = PdfiumEngineProvider.bridge.getPageObjectCount(pagePtr)
|
||||
val imgRects = mutableListOf<android.graphics.Rect>()
|
||||
val outRect = FloatArray(4)
|
||||
|
||||
for (i in 0 until objCount) {
|
||||
if (NativePdfiumBridge.getPageObjectType(pagePtr, i) == 3) { // 3 = FPDF_PAGEOBJ_IMAGE
|
||||
if (NativePdfiumBridge.getPageObjectBoundingBox(pagePtr, i, outRect)) {
|
||||
if (PdfiumEngineProvider.bridge.getPageObjectType(pagePtr, i) == 3) { // 3 = FPDF_PAGEOBJ_IMAGE
|
||||
if (PdfiumEngineProvider.bridge.getPageObjectBoundingBox(pagePtr, i, outRect)) {
|
||||
val pdfRectF = android.graphics.RectF(
|
||||
min(outRect[0], outRect[2]),
|
||||
max(outRect[1], outRect[3]),
|
||||
|
|
@ -1180,26 +1211,26 @@ internal fun PdfPageComposable(
|
|||
val pagePtr = pageWrapper.getNativePointer()
|
||||
|
||||
if (pagePtr != 0L) {
|
||||
val count = NativePdfiumBridge.getAnnotCount(pagePtr)
|
||||
val count = PdfiumEngineProvider.bridge.getAnnotCount(pagePtr)
|
||||
Timber.tag("PdfCommentDebug").d("Page $pageIndex: Total Annotations found = $count")
|
||||
if (count > 0) {
|
||||
val count = NativePdfiumBridge.getAnnotCount(pagePtr)
|
||||
val count = PdfiumEngineProvider.bridge.getAnnotCount(pagePtr)
|
||||
Timber.tag("PdfCommentDebug").d("Page $pageIndex: Total Annotations found = $count")
|
||||
if (count > 0) {
|
||||
val allAnnots = (0 until count).mapNotNull { i ->
|
||||
val subtype = NativePdfiumBridge.getAnnotSubtype(pagePtr, i)
|
||||
val subtype = PdfiumEngineProvider.bridge.getAnnotSubtype(pagePtr, i)
|
||||
if (subtype == annotLink) return@mapNotNull null
|
||||
|
||||
var contents = NativePdfiumBridge.getAnnotString(pagePtr, i, "Contents")
|
||||
var contents = PdfiumEngineProvider.bridge.getAnnotString(pagePtr, i, "Contents")
|
||||
if (contents.isNullOrBlank()) {
|
||||
contents = NativePdfiumBridge.getAnnotString(pagePtr, i, "RC")
|
||||
contents = PdfiumEngineProvider.bridge.getAnnotString(pagePtr, i, "RC")
|
||||
}
|
||||
|
||||
val name = NativePdfiumBridge.getAnnotString(pagePtr, i, "NM")
|
||||
val irt = NativePdfiumBridge.getAnnotString(pagePtr, i, "IRT")
|
||||
val author = NativePdfiumBridge.getAnnotString(pagePtr, i, "T")
|
||||
val name = PdfiumEngineProvider.bridge.getAnnotString(pagePtr, i, "NM")
|
||||
val irt = PdfiumEngineProvider.bridge.getAnnotString(pagePtr, i, "IRT")
|
||||
val author = PdfiumEngineProvider.bridge.getAnnotString(pagePtr, i, "T")
|
||||
|
||||
val pdfRectArray = NativePdfiumBridge.getAnnotRect(pagePtr, i)
|
||||
val pdfRectArray = PdfiumEngineProvider.bridge.getAnnotRect(pagePtr, i)
|
||||
val pdfRectF = if (pdfRectArray != null) {
|
||||
android.graphics.RectF(
|
||||
min(pdfRectArray[0], pdfRectArray[2]),
|
||||
|
|
@ -1311,8 +1342,7 @@ internal fun PdfPageComposable(
|
|||
}
|
||||
|
||||
LaunchedEffect(
|
||||
effectiveScale,
|
||||
effectiveOffset,
|
||||
needsTilingNow,
|
||||
actualBitmapWidthPx,
|
||||
actualBitmapHeightPx,
|
||||
canvasWidthPx.floatValue,
|
||||
|
|
@ -1323,8 +1353,7 @@ internal fun PdfPageComposable(
|
|||
virtualPage,
|
||||
isActivePage
|
||||
) {
|
||||
val needsTiling = (effectiveScale > 1f || actualBitmapWidthPx > 3000 || actualBitmapHeightPx > 3000) && (isVerticalScroll || isActivePage)
|
||||
if (!needsTiling) {
|
||||
if (!needsTilingNow) {
|
||||
if (tiles.isNotEmpty()) {
|
||||
val oldTiles = tiles
|
||||
tiles = emptyList()
|
||||
|
|
@ -1358,24 +1387,37 @@ internal fun PdfPageComposable(
|
|||
|
||||
snapshotFlow {
|
||||
val rect = visibleScreenRect()
|
||||
if (rect == null) null
|
||||
else {
|
||||
val observedScale = latestEffectiveScale
|
||||
if (isVerticalScroll && rect != null) {
|
||||
val qTop = rect.top / (tileSizePx / 2)
|
||||
val qLeft = rect.left / (tileSizePx / 2)
|
||||
val qBottom = rect.bottom / (tileSizePx / 2)
|
||||
val qRight = rect.right / (tileSizePx / 2)
|
||||
listOf(qTop, qLeft, qBottom, qRight)
|
||||
listOf(qTop, qLeft, qBottom, qRight, (observedScale * 10f).roundToInt())
|
||||
} else if (!isVerticalScroll) {
|
||||
val observedOffset = latestEffectiveOffset
|
||||
val pivotX = screenWidth / 2f
|
||||
val pivotY = screenHeight / 2f
|
||||
val pxTl = (((0 - observedOffset.x) - pivotX) / observedScale + pivotX) - centeringOffsetX
|
||||
val pyTl = (((0 - observedOffset.y) - pivotY) / observedScale + pivotY) - centeringOffsetY
|
||||
val pxBr = (((screenWidth - observedOffset.x) - pivotX) / observedScale + pivotX) - centeringOffsetX
|
||||
val pyBr = (((screenHeight - observedOffset.y) - pivotY) / observedScale + pivotY) - centeringOffsetY
|
||||
|
||||
val qTop = pyTl.toInt() / (tileSizePx / 2)
|
||||
val qLeft = pxTl.toInt() / (tileSizePx / 2)
|
||||
val qBottom = pyBr.toInt() / (tileSizePx / 2)
|
||||
val qRight = pxBr.toInt() / (tileSizePx / 2)
|
||||
listOf(qTop, qLeft, qBottom, qRight, (observedScale * 10f).roundToInt())
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.conflate().collectLatest { _ ->
|
||||
|
||||
delay(150)
|
||||
|
||||
val tileCalcStart = System.nanoTime()
|
||||
if (!isActive) return@collectLatest
|
||||
|
||||
if (isScrolling && effectiveScale > 1f) {
|
||||
return@collectLatest
|
||||
}
|
||||
val renderScale = latestEffectiveScale
|
||||
val renderOffset = latestEffectiveOffset
|
||||
|
||||
val currentVisibleRect = visibleScreenRect()
|
||||
|
||||
|
|
@ -1404,14 +1446,14 @@ internal fun PdfPageComposable(
|
|||
val pivotX = screenWidth / 2f
|
||||
val pivotY = screenHeight / 2f
|
||||
|
||||
pxTl = (((0 - effectiveOffset.x) - pivotX) / effectiveScale + pivotX) - centeringOffsetX
|
||||
pyTl = (((0 - effectiveOffset.y) - pivotY) / effectiveScale + pivotY) - centeringOffsetY
|
||||
pxBr = (((screenWidth - effectiveOffset.x) - pivotX) / effectiveScale + pivotX) - centeringOffsetX
|
||||
pyBr = (((screenHeight - effectiveOffset.y) - pivotY) / effectiveScale + pivotY) - centeringOffsetY
|
||||
pxTl = (((0 - renderOffset.x) - pivotX) / renderScale + pivotX) - centeringOffsetX
|
||||
pyTl = (((0 - renderOffset.y) - pivotY) / renderScale + pivotY) - centeringOffsetY
|
||||
pxBr = (((screenWidth - renderOffset.x) - pivotX) / renderScale + pivotX) - centeringOffsetX
|
||||
pyBr = (((screenHeight - renderOffset.y) - pivotY) / renderScale + pivotY) - centeringOffsetY
|
||||
}
|
||||
|
||||
val visibleBitmapRect = Rect(pxTl.toInt(), pyTl.toInt(), pxBr.toInt(), pyBr.toInt())
|
||||
val inset = if (effectiveScale > 2f) 0 else -tileSizePx
|
||||
val inset = if (renderScale > 2f) 0 else -tileSizePx
|
||||
visibleBitmapRect.inset(inset, inset)
|
||||
|
||||
val requiredTileIds = mutableSetOf<Int>()
|
||||
|
|
@ -1431,8 +1473,10 @@ internal fun PdfPageComposable(
|
|||
|
||||
val currentTileIds = tiles.map { it.tileId }.toSet()
|
||||
|
||||
val scaleTolerance = 0.05f
|
||||
val validCurrentTileIds = tiles.filter { abs(it.renderScale - effectiveScale) <= scaleTolerance }.map { it.tileId }.toSet()
|
||||
val scaleTolerance = PDF_TILE_SCALE_TOLERANCE
|
||||
val validCurrentTileIds = tiles.filter { abs(it.renderScale - renderScale) <= scaleTolerance }.map { it.tileId }.toSet()
|
||||
val tilesToRenderIds = requiredTileIds - validCurrentTileIds
|
||||
val tilesToRecycleIds = currentTileIds - requiredTileIds
|
||||
|
||||
val duration = (System.nanoTime() - tileCalcStart) / 1_000_000f
|
||||
if (duration > 2f) {
|
||||
|
|
@ -1441,21 +1485,26 @@ internal fun PdfPageComposable(
|
|||
)
|
||||
}
|
||||
|
||||
if (requiredTileIds != validCurrentTileIds) {
|
||||
|
||||
val tilesToRenderIds = requiredTileIds - validCurrentTileIds
|
||||
val tilesToRecycleIds = currentTileIds - requiredTileIds
|
||||
|
||||
if (tilesToRecycleIds.isNotEmpty()) {
|
||||
val (tilesToRecycle, tilesToKeep) = tiles.partition { it.tileId in tilesToRecycleIds }
|
||||
tiles = tilesToKeep
|
||||
withContext(Dispatchers.IO) {
|
||||
tilesToRecycle.forEach { PdfBitmapPool.recycle(it.bitmap) }
|
||||
}
|
||||
if (tilesToRecycleIds.isNotEmpty()) {
|
||||
val (tilesToRecycle, tilesToKeep) = tiles.partition { it.tileId in tilesToRecycleIds }
|
||||
tiles = tilesToKeep
|
||||
withContext(Dispatchers.IO) {
|
||||
tilesToRecycle.forEach { PdfBitmapPool.recycle(it.bitmap) }
|
||||
}
|
||||
}
|
||||
|
||||
if (isScrolling && renderScale > 1f) {
|
||||
return@collectLatest
|
||||
}
|
||||
|
||||
if (requiredTileIds != validCurrentTileIds) {
|
||||
if (tilesToRenderIds.isNotEmpty()) {
|
||||
withContext(Dispatchers.IO) {
|
||||
delay(PDF_TILE_IDLE_RENDER_DELAY_MS)
|
||||
if (!isActive) return@collectLatest
|
||||
if (isScrolling && latestEffectiveScale > 1f) return@collectLatest
|
||||
|
||||
val renderedTiles = withContext(Dispatchers.IO) {
|
||||
val newTiles = mutableListOf<PdfTile>()
|
||||
tilesToRenderIds.forEach { tileId ->
|
||||
if (!isActive) return@forEach
|
||||
|
||||
|
|
@ -1469,14 +1518,18 @@ internal fun PdfPageComposable(
|
|||
(col + 1) * tileSizePx,
|
||||
(row + 1) * tileSizePx
|
||||
)
|
||||
val tileRenderSize = (tileSizePx * effectiveScale).toInt().coerceAtLeast(1)
|
||||
val tileRenderScale = min(
|
||||
renderScale,
|
||||
PDF_MAX_TILE_BITMAP_SIZE_PX.toFloat() / tileSizePx.toFloat()
|
||||
)
|
||||
val tileRenderSize = (tileSizePx * tileRenderScale).toInt().coerceAtLeast(1)
|
||||
|
||||
val tileBitmap = PdfBitmapPool.get(tileRenderSize)
|
||||
|
||||
val fullPageRenderWidth = (actualBitmapWidthPx * effectiveScale).toInt()
|
||||
val fullPageRenderHeight = (actualBitmapHeightPx * effectiveScale).toInt()
|
||||
val tileRenderX = (col * tileSizePx * effectiveScale).toInt()
|
||||
val tileRenderY = (row * tileSizePx * effectiveScale).toInt()
|
||||
val fullPageRenderWidth = (actualBitmapWidthPx * tileRenderScale).toInt()
|
||||
val fullPageRenderHeight = (actualBitmapHeightPx * tileRenderScale).toInt()
|
||||
val tileRenderX = (col * tileSizePx * tileRenderScale).toInt()
|
||||
val tileRenderY = (row * tileSizePx * tileRenderScale).toInt()
|
||||
|
||||
page?.renderPageBitmap(
|
||||
bitmap = tileBitmap,
|
||||
|
|
@ -1487,24 +1540,26 @@ internal fun PdfPageComposable(
|
|||
renderAnnot = true
|
||||
)
|
||||
|
||||
val newTile = PdfTile(tileBitmap, tileRect, tileId, effectiveScale)
|
||||
var handedOver = false
|
||||
try {
|
||||
withContext(Dispatchers.Main) {
|
||||
val oldTile = tiles.find { it.tileId == tileId }
|
||||
tiles = tiles.filter { it.tileId != tileId } + newTile
|
||||
handedOver = true
|
||||
newTiles += PdfTile(tileBitmap, tileRect, tileId, renderScale)
|
||||
}
|
||||
newTiles
|
||||
}
|
||||
|
||||
oldTile?.let {
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
PdfBitmapPool.recycle(it.bitmap)
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (!handedOver) {
|
||||
PdfBitmapPool.recycle(tileBitmap)
|
||||
}
|
||||
if (!isActive) {
|
||||
withContext(Dispatchers.IO) {
|
||||
renderedTiles.forEach { PdfBitmapPool.recycle(it.bitmap) }
|
||||
}
|
||||
return@collectLatest
|
||||
}
|
||||
|
||||
if (renderedTiles.isNotEmpty()) {
|
||||
val renderedIds = renderedTiles.map { it.tileId }.toSet()
|
||||
val replacedTiles = tiles.filter { it.tileId in renderedIds }
|
||||
tiles = tiles.filterNot { it.tileId in renderedIds } + renderedTiles
|
||||
|
||||
if (replacedTiles.isNotEmpty()) {
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
replacedTiles.forEach { PdfBitmapPool.recycle(it.bitmap) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2799,7 +2854,7 @@ internal fun PdfPageComposable(
|
|||
|
||||
Timber.tag("PdfLinkDiagnostic").i("Extracted docPtr: $docPtr | pagePtr: $pagePtr")
|
||||
|
||||
val linkInfo = NativePdfiumBridge.getLinkInfoAtPoint(
|
||||
val linkInfo = PdfiumEngineProvider.bridge.getLinkInfoAtPoint(
|
||||
docPtr, pagePtr, pdfCoords.x.toDouble(), pdfCoords.y.toDouble()
|
||||
)
|
||||
|
||||
|
|
@ -2818,7 +2873,7 @@ internal fun PdfPageComposable(
|
|||
}
|
||||
}
|
||||
|
||||
val clickHandled = NativePdfiumBridge.performClick(pagePtr, pdfCoords.x.toDouble(), pdfCoords.y.toDouble())
|
||||
val clickHandled = PdfiumEngineProvider.bridge.performClick(pagePtr, pdfCoords.x.toDouble(), pdfCoords.y.toDouble())
|
||||
if (clickHandled) {
|
||||
return@withContext 2
|
||||
}
|
||||
|
|
@ -3007,25 +3062,21 @@ internal fun PdfPageComposable(
|
|||
awaitEachGesture {
|
||||
@Suppress("UnusedVariable", "Unused") val down =
|
||||
awaitFirstDown(requireUnconsumed = false)
|
||||
paginationPanFlingJob?.cancel()
|
||||
paginationPanFlingJob = null
|
||||
velocityTracker.resetTracking()
|
||||
|
||||
var mode = 0
|
||||
var accumulatedZoom = 1f
|
||||
var accumulatedPan = Offset.Zero
|
||||
var swipeAccumulatorX = 0f
|
||||
var velocityAccumulator = Offset.Zero
|
||||
|
||||
do {
|
||||
val event = awaitPointerEvent()
|
||||
val canceled = event.changes.any { it.isConsumed }
|
||||
val pointerCount = event.changes.size
|
||||
|
||||
val currentCentroid = event.calculateCentroid(useCurrent = true)
|
||||
if (pointerCount > 0 && currentCentroid != Offset.Unspecified) {
|
||||
velocityTracker.addPosition(
|
||||
event.changes[0].uptimeMillis, currentCentroid
|
||||
)
|
||||
}
|
||||
|
||||
if (!canceled) {
|
||||
val rawPanChange = event.calculatePan()
|
||||
val panChange = if (isScrollLocked && pointerCount == 1) {
|
||||
|
|
@ -3077,6 +3128,14 @@ internal fun PdfPageComposable(
|
|||
Timber.tag("PdfZoomDebug").v("Panning: Offset $offset -> $newX, $newY (Max: $maxOffsetX, $maxOffsetY)")
|
||||
offset = Offset(newX, newY)
|
||||
|
||||
if (event.changes.isNotEmpty() && panChange != Offset.Zero) {
|
||||
velocityAccumulator += panChange
|
||||
velocityTracker.addPosition(
|
||||
event.changes[0].uptimeMillis,
|
||||
velocityAccumulator
|
||||
)
|
||||
}
|
||||
|
||||
event.changes.forEach {
|
||||
if (it.positionChanged()) it.consume()
|
||||
}
|
||||
|
|
@ -3189,38 +3248,54 @@ internal fun PdfPageComposable(
|
|||
}
|
||||
}
|
||||
} else if (mode == 1 && scale > 1f) {
|
||||
val velocity = velocityTracker.calculateVelocity()
|
||||
val contentWidth = actualBitmapWidthPx * scale
|
||||
val contentHeight = actualBitmapHeightPx * scale
|
||||
val maxOffsetX = (contentWidth - size.width).coerceAtLeast(0f) / 2f
|
||||
val maxOffsetY = (contentHeight - size.height).coerceAtLeast(0f) / 2f
|
||||
|
||||
val startX = offset.x
|
||||
val startY = offset.y
|
||||
val velocity = velocityTracker.calculateVelocity()
|
||||
val flingX = if (!isScrollLocked && abs(velocity.x) > PDF_PAGINATION_PAN_FLING_MIN_VELOCITY) {
|
||||
velocity.x * PDF_PAGINATION_PAN_FLING_MULTIPLIER
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
val flingY = if (abs(velocity.y) > PDF_PAGINATION_PAN_FLING_MIN_VELOCITY) {
|
||||
velocity.y * PDF_PAGINATION_PAN_FLING_MULTIPLIER
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
|
||||
coroutineScope.launch {
|
||||
coroutineScope {
|
||||
launch {
|
||||
if (!isScrollLocked) {
|
||||
Animatable(startX).animateDecay(
|
||||
velocity.x, decay
|
||||
) {
|
||||
val newX = value.coerceIn(
|
||||
-maxOffsetX, maxOffsetX
|
||||
)
|
||||
offset = offset.copy(x = newX)
|
||||
if (flingX == 0f && flingY == 0f) {
|
||||
offset = Offset(
|
||||
x = offset.x.coerceIn(-maxOffsetX, maxOffsetX),
|
||||
y = offset.y.coerceIn(-maxOffsetY, maxOffsetY)
|
||||
)
|
||||
} else {
|
||||
val startOffset = offset
|
||||
paginationPanFlingJob = coroutineScope.launch {
|
||||
try {
|
||||
coroutineScope {
|
||||
launch {
|
||||
if (flingX != 0f) {
|
||||
Animatable(startOffset.x).animateDecay(flingX, decay) {
|
||||
offset = offset.copy(
|
||||
x = value.coerceIn(-maxOffsetX, maxOffsetX)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
launch {
|
||||
if (flingY != 0f) {
|
||||
Animatable(startOffset.y).animateDecay(flingY, decay) {
|
||||
offset = offset.copy(
|
||||
y = value.coerceIn(-maxOffsetY, maxOffsetY)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
launch {
|
||||
Animatable(startY).animateDecay(
|
||||
velocity.y, decay
|
||||
) {
|
||||
val newY = value.coerceIn(
|
||||
-maxOffsetY, maxOffsetY
|
||||
)
|
||||
offset = offset.copy(y = newY)
|
||||
}
|
||||
} finally {
|
||||
paginationPanFlingJob = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3256,7 +3331,7 @@ internal fun PdfPageComposable(
|
|||
}
|
||||
|
||||
val buttons = currentEvent.buttons
|
||||
Timber.tag("StylusEraserDiagnostic").d(
|
||||
Timber.tag("StylusDebug").d(
|
||||
"Page $pageIndex | Type: ${down.type} | isPrimary: ${buttons.isPrimaryPressed} | isSecondary: ${buttons.isSecondaryPressed} | isTertiary: ${buttons.isTertiaryPressed} | buttonsString: $buttons"
|
||||
)
|
||||
|
||||
|
|
@ -3881,7 +3956,10 @@ internal fun PdfPageComposable(
|
|||
stableColorFilter,
|
||||
isDarkMode,
|
||||
excludeImages,
|
||||
stableImageRects
|
||||
stableImageRects,
|
||||
textureBitmap,
|
||||
effectiveTextureAlpha,
|
||||
textureBlendMode
|
||||
) {
|
||||
Timber.tag("PdfDrawPerf").v(
|
||||
"STATIC DATA GENERATED: Scale=$effectiveScale, Tiles=${stableTiles.item.size}"
|
||||
|
|
@ -3899,7 +3977,10 @@ internal fun PdfPageComposable(
|
|||
colorFilter = stableColorFilter,
|
||||
isDarkMode = isDarkMode,
|
||||
excludeImages = excludeImages,
|
||||
imageRects = stableImageRects
|
||||
imageRects = stableImageRects,
|
||||
textureBitmap = StableHolder(textureBitmap),
|
||||
textureAlpha = effectiveTextureAlpha,
|
||||
textureBlendMode = textureBlendMode
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -4263,7 +4344,10 @@ private fun PdfBitmapLayer(
|
|||
colorFilter: ColorFilter? = null,
|
||||
isDarkMode: Boolean = false,
|
||||
excludeImages: Boolean = false,
|
||||
imageRects: List<android.graphics.Rect> = emptyList()
|
||||
imageRects: List<android.graphics.Rect> = emptyList(),
|
||||
textureBitmap: ImageBitmap? = null,
|
||||
textureAlpha: Float = 0f,
|
||||
textureBlendMode: BlendMode = BlendMode.Multiply
|
||||
) {
|
||||
Canvas(modifier = Modifier.fillMaxSize().graphicsLayer()) {
|
||||
translate(left = centeringOffsetX, top = centeringOffsetY) {
|
||||
|
|
@ -4363,6 +4447,15 @@ private fun PdfBitmapLayer(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (textureBitmap != null && textureAlpha > 0f) {
|
||||
drawRect(
|
||||
brush = ShaderBrush(ImageShader(textureBitmap, TileMode.Repeated, TileMode.Repeated)),
|
||||
size = Size(dstW.toFloat(), dstH.toFloat()),
|
||||
blendMode = textureBlendMode,
|
||||
alpha = textureAlpha
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4884,7 +4977,10 @@ private fun PdfPageStaticLayer(data: PageStaticData) {
|
|||
colorFilter = data.colorFilter.item,
|
||||
isDarkMode = data.isDarkMode,
|
||||
excludeImages = data.excludeImages,
|
||||
imageRects = data.imageRects.item
|
||||
imageRects = data.imageRects.item,
|
||||
textureBitmap = data.textureBitmap.item,
|
||||
textureAlpha = data.textureAlpha,
|
||||
textureBlendMode = data.textureBlendMode
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import androidx.compose.ui.graphics.toArgb
|
|||
import androidx.core.content.edit
|
||||
import com.aryan.reader.BuildConfig
|
||||
import com.aryan.reader.ReaderTheme
|
||||
import com.aryan.reader.ReaderTexture
|
||||
import com.aryan.reader.epubreader.SystemUiMode
|
||||
|
||||
internal const val VERTICAL_SCROLL_TAG = "PdfVerticalScroll"
|
||||
|
|
@ -39,6 +40,8 @@ private const val PREF_EXTERNAL_SEARCH_PKG = "external_search_package"
|
|||
private const val PDF_THEME_KEY = "pdf_reader_theme"
|
||||
private const val PDF_KEEP_SCREEN_ON_KEY = "pdf_keep_screen_on_enabled"
|
||||
private const val PDF_HIDDEN_TOOLS_KEY = "pdf_hidden_tools"
|
||||
private const val PDF_TOOL_ORDER_KEY = "pdf_tool_order"
|
||||
private const val PDF_BOTTOM_TOOLS_KEY = "pdf_bottom_tools"
|
||||
private const val PDF_SYSTEM_UI_MODE_KEY = "pdf_system_ui_mode"
|
||||
internal const val PDF_LAYOUT_DEBUG_TAG = "PdfLayoutDebug"
|
||||
|
||||
|
|
@ -76,7 +79,13 @@ val PdfBuiltInThemes = listOf(
|
|||
ReaderTheme("dark", "Dark", Color(0xFF121212), Color(0xFFE0E0E0), true),
|
||||
ReaderTheme("sepia", "Sepia", Color(0xFFFBF0D9), Color(0xFF5F4B32), false),
|
||||
ReaderTheme("slate", "Slate", Color(0xFF2E3440), Color(0xFFECEFF4), true),
|
||||
ReaderTheme("oled", "OLED", Color(0xFF000000), Color(0xFFB0B0B0), true)
|
||||
ReaderTheme("oled", "OLED", Color(0xFF000000), Color(0xFFB0B0B0), true),
|
||||
ReaderTheme("pdf_natural_white_texture", "Natural White", Color(0xFFF7F1E5), Color(0xFF1D1B18), false, textureId = ReaderTexture.NATURAL_WHITE.id),
|
||||
ReaderTheme("pdf_retina_texture", "Retina", Color(0xFFF1E4CD), Color(0xFF2A2119), false, textureId = ReaderTexture.RETINA_WOOD.id),
|
||||
ReaderTheme("pdf_veneer_texture", "Veneer", Color(0xFFF4E7CF), Color(0xFF2A2119), false, textureId = ReaderTexture.LIGHT_VENEER.id),
|
||||
ReaderTheme("pdf_grey_wash_texture", "Grey Wash", Color(0xFF202124), Color(0xFFFFFFFF), true, textureId = ReaderTexture.GREY_WASH.id),
|
||||
ReaderTheme("pdf_fabric_texture", "Fabric", Color(0xFF262626), Color(0xFFE8E2D8), true, textureId = ReaderTexture.CLASSY_FABRIC.id),
|
||||
ReaderTheme("pdf_retro_texture", "Retro", Color(0xFFF6ECD8), Color(0xFF2F2118), false, textureId = ReaderTexture.RETRO_INTRO.id)
|
||||
)
|
||||
|
||||
internal fun loadPdfHiddenTools(context: Context): Set<String> {
|
||||
|
|
@ -89,6 +98,32 @@ internal fun savePdfHiddenTools(context: Context, hiddenTools: Set<String>) {
|
|||
prefs.edit { putStringSet(PDF_HIDDEN_TOOLS_KEY, hiddenTools) }
|
||||
}
|
||||
|
||||
internal fun loadPdfToolOrder(context: Context): List<PdfReaderTool> {
|
||||
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
|
||||
val savedTools = prefs.getString(PDF_TOOL_ORDER_KEY, null)
|
||||
?.split(',')
|
||||
?.filter { it.isNotBlank() }
|
||||
?.mapNotNull { name -> PdfReaderTool.entries.firstOrNull { it.name == name } }
|
||||
.orEmpty()
|
||||
return (savedTools + PdfReaderTool.entries.filterNot { it in savedTools }).distinct()
|
||||
}
|
||||
|
||||
internal fun savePdfToolOrder(context: Context, toolOrder: List<PdfReaderTool>) {
|
||||
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
|
||||
prefs.edit { putString(PDF_TOOL_ORDER_KEY, toolOrder.joinToString(",") { it.name }) }
|
||||
}
|
||||
|
||||
internal fun loadPdfBottomTools(context: Context): Set<String> {
|
||||
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
|
||||
val defaultBottomTools = PdfReaderTool.entries.filter { it.category == "Bottom Bar" }.map { it.name }.toSet()
|
||||
return prefs.getStringSet(PDF_BOTTOM_TOOLS_KEY, defaultBottomTools) ?: defaultBottomTools
|
||||
}
|
||||
|
||||
internal fun savePdfBottomTools(context: Context, bottomTools: Set<String>) {
|
||||
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
|
||||
prefs.edit { putStringSet(PDF_BOTTOM_TOOLS_KEY, bottomTools) }
|
||||
}
|
||||
|
||||
internal fun loadCustomHighlightColors(context: Context): Map<PdfHighlightColor, Color> {
|
||||
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
|
||||
return PdfHighlightColor.entries.associateWith {
|
||||
|
|
@ -139,7 +174,7 @@ internal fun loadPdfThemeId(context: Context): String {
|
|||
}
|
||||
|
||||
internal fun loadUseOnlineDict(context: Context): Boolean {
|
||||
@Suppress("KotlinConstantConditions") if (BuildConfig.FLAVOR == "oss") return false
|
||||
@Suppress("KotlinConstantConditions") if (BuildConfig.FLAVOR == "oss" && BuildConfig.IS_OFFLINE) return false
|
||||
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
|
||||
return prefs.getBoolean(PREF_USE_ONLINE_DICT, true)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
@file:kotlin.OptIn(ExperimentalMaterial3Api::class)
|
||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||
|
||||
package com.aryan.reader.pdf
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
|
|
@ -11,94 +12,346 @@ import androidx.compose.foundation.layout.WindowInsets
|
|||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.detectDragGestures
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.zIndex
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
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.Search
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.layout.boundsInWindow
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.aryan.reader.R
|
||||
import com.aryan.reader.epubreader.OptionSegmentedControl
|
||||
import com.aryan.reader.epubreader.SystemUiMode
|
||||
|
||||
|
||||
enum class PdfFlatItemType { SECTION_HEADER, TOOL, EMPTY_PLACEHOLDER, MORE_HEADER, MORE_TOOL }
|
||||
|
||||
data class PdfFlatToolItem(
|
||||
val id: String,
|
||||
val type: PdfFlatItemType,
|
||||
val tool: PdfReaderTool? = null,
|
||||
val section: PdfToolbarSection? = null,
|
||||
val title: String? = null
|
||||
)
|
||||
|
||||
fun sanitizePdfPlaceholders(list: List<PdfFlatToolItem>): List<PdfFlatToolItem> {
|
||||
val result = mutableListOf<PdfFlatToolItem>()
|
||||
val sectionMap = mutableMapOf<PdfToolbarSection, MutableList<PdfFlatToolItem>>()
|
||||
PdfToolbarSection.entries.forEach { sectionMap[it] = mutableListOf() }
|
||||
|
||||
list.forEach { item ->
|
||||
if (item.type == PdfFlatItemType.TOOL) {
|
||||
item.section?.let { sectionMap[it]?.add(item) }
|
||||
}
|
||||
}
|
||||
|
||||
PdfToolbarSection.entries.forEach { section ->
|
||||
result.add(PdfFlatToolItem("header_${section.name}", PdfFlatItemType.SECTION_HEADER, section = section, title = section.title))
|
||||
val tools = sectionMap[section] ?: emptyList()
|
||||
if (tools.isEmpty()) {
|
||||
result.add(PdfFlatToolItem("empty_${section.name}", PdfFlatItemType.EMPTY_PLACEHOLDER, section = section))
|
||||
} else {
|
||||
result.addAll(tools)
|
||||
}
|
||||
}
|
||||
|
||||
list.filter { it.type == PdfFlatItemType.MORE_HEADER || it.type == PdfFlatItemType.MORE_TOOL }.forEach {
|
||||
result.add(it)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
class PdfDragDropState(
|
||||
val lazyListState: LazyListState,
|
||||
val onMove: (String, String) -> Unit
|
||||
) {
|
||||
var draggedItemId by mutableStateOf<String?>(null)
|
||||
var dragOffset by mutableStateOf(Offset.Zero)
|
||||
|
||||
fun onDragStart(id: String) { draggedItemId = id; dragOffset = Offset.Zero }
|
||||
fun onDrag(delta: Offset) {
|
||||
val draggedId = draggedItemId ?: return
|
||||
dragOffset += delta
|
||||
val visibleItems = lazyListState.layoutInfo.visibleItemsInfo
|
||||
val currentItem = visibleItems.find { it.key == draggedId } ?: return
|
||||
val center = currentItem.offset + dragOffset.y + currentItem.size / 2f
|
||||
val targetItem = visibleItems.find { it.key != draggedId && center >= it.offset && center <= (it.offset + it.size) }
|
||||
if (targetItem != null) {
|
||||
onMove(draggedId, targetItem.key.toString())
|
||||
dragOffset = dragOffset.copy(y = dragOffset.y - (targetItem.offset - currentItem.offset))
|
||||
}
|
||||
}
|
||||
fun onDragEnd() { draggedItemId = null; dragOffset = Offset.Zero }
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun PdfCustomizeToolsSheet(
|
||||
hiddenTools: Set<String>,
|
||||
toolOrder: List<PdfReaderTool>,
|
||||
bottomTools: Set<String>,
|
||||
onUpdate: (Set<String>) -> Unit,
|
||||
onOrderUpdate: (List<PdfReaderTool>) -> Unit,
|
||||
onPlacementUpdate: (Set<String>) -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismiss,
|
||||
contentWindowInsets = { WindowInsets.navigationBars }
|
||||
) {
|
||||
Column(modifier = Modifier.padding(horizontal = 24.dp).padding(bottom = 24.dp)) {
|
||||
Text(
|
||||
text = stringResource(R.string.title_customize_toolbar),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.desc_customize_toolbar),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
val reorderableToolbarTools = setOf(
|
||||
PdfReaderTool.DICTIONARY, PdfReaderTool.THEME, PdfReaderTool.LOCK_PANNING,
|
||||
PdfReaderTool.SLIDER, PdfReaderTool.TOC, PdfReaderTool.SEARCH,
|
||||
PdfReaderTool.HIGHLIGHT_ALL, PdfReaderTool.AI_FEATURES,
|
||||
PdfReaderTool.EDIT_MODE, PdfReaderTool.TTS_CONTROLS
|
||||
)
|
||||
|
||||
LazyColumn(modifier = Modifier.fillMaxWidth()) {
|
||||
PdfReaderTool.entries.groupBy { it.category }.forEach { (category, tools) ->
|
||||
item {
|
||||
Text(
|
||||
text = category,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(top = 16.dp, bottom = 8.dp)
|
||||
)
|
||||
var localHiddenTools by remember { mutableStateOf(hiddenTools) }
|
||||
var flatItems by remember {
|
||||
mutableStateOf<List<PdfFlatToolItem>>(
|
||||
run {
|
||||
val toolbarTools = toolOrder.filter { it in reorderableToolbarTools }
|
||||
val topTools = toolbarTools.filter { !bottomTools.contains(it.name) && !hiddenTools.contains(it.name) }
|
||||
val bottomToolsList = toolbarTools.filter { bottomTools.contains(it.name) && !hiddenTools.contains(it.name) }
|
||||
val hiddenToolsList = toolbarTools.filter { hiddenTools.contains(it.name) }
|
||||
val moreTools = toolOrder.filter { it !in reorderableToolbarTools }
|
||||
|
||||
val list = mutableListOf<PdfFlatToolItem>()
|
||||
|
||||
PdfToolbarSection.entries.forEach { section ->
|
||||
val tools = when(section) {
|
||||
PdfToolbarSection.TOP -> topTools
|
||||
PdfToolbarSection.BOTTOM -> bottomToolsList
|
||||
PdfToolbarSection.HIDDEN -> hiddenToolsList
|
||||
}
|
||||
items(tools) { tool ->
|
||||
Row(
|
||||
list.add(PdfFlatToolItem("header_${section.name}", PdfFlatItemType.SECTION_HEADER, section = section, title = section.title))
|
||||
if (tools.isEmpty()) {
|
||||
list.add(PdfFlatToolItem("empty_${section.name}", PdfFlatItemType.EMPTY_PLACEHOLDER, section = section))
|
||||
} else {
|
||||
tools.forEach { tool ->
|
||||
list.add(PdfFlatToolItem("tool_${tool.name}", PdfFlatItemType.TOOL, tool = tool, section = section))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
list.add(PdfFlatToolItem("more_header", PdfFlatItemType.MORE_HEADER, title = "More menu"))
|
||||
moreTools.forEach { tool ->
|
||||
list.add(PdfFlatToolItem("more_${tool.name}", PdfFlatItemType.MORE_TOOL, tool = tool))
|
||||
}
|
||||
list
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val commitDragDrop = {
|
||||
val newHidden = localHiddenTools.filter { toolName ->
|
||||
toolOrder.find { it.name == toolName } !in reorderableToolbarTools
|
||||
}.toMutableSet()
|
||||
|
||||
val newBottom = mutableSetOf<String>()
|
||||
val newOrder = mutableListOf<PdfReaderTool>()
|
||||
|
||||
flatItems.forEach { item ->
|
||||
if (item.type == PdfFlatItemType.TOOL && item.tool != null) {
|
||||
newOrder.add(item.tool)
|
||||
if (item.section == PdfToolbarSection.HIDDEN) newHidden.add(item.tool.name)
|
||||
if (item.section == PdfToolbarSection.BOTTOM) newBottom.add(item.tool.name)
|
||||
}
|
||||
}
|
||||
|
||||
val moreTools = flatItems.filter { it.type == PdfFlatItemType.MORE_TOOL }.mapNotNull { it.tool }
|
||||
newOrder.addAll(moreTools)
|
||||
|
||||
localHiddenTools = newHidden
|
||||
onUpdate(newHidden)
|
||||
onPlacementUpdate(newBottom)
|
||||
onOrderUpdate(newOrder)
|
||||
}
|
||||
|
||||
val lazyListState = rememberLazyListState()
|
||||
val dragDropState = remember {
|
||||
PdfDragDropState(lazyListState) { fromKey, toKey ->
|
||||
val fromIndex = flatItems.indexOfFirst { it.id == fromKey }
|
||||
val toIndex = flatItems.indexOfFirst { it.id == toKey }
|
||||
if (fromIndex == -1 || toIndex == -1 || fromIndex == toIndex) return@PdfDragDropState
|
||||
|
||||
val fromItem = flatItems[fromIndex]
|
||||
if (fromItem.type != PdfFlatItemType.TOOL) return@PdfDragDropState
|
||||
|
||||
val toItem = flatItems[toIndex]
|
||||
if (toItem.type == PdfFlatItemType.MORE_HEADER || toItem.type == PdfFlatItemType.MORE_TOOL) return@PdfDragDropState
|
||||
|
||||
val newList = flatItems.toMutableList()
|
||||
val movedItem = newList.removeAt(fromIndex)
|
||||
|
||||
val newToIndex = newList.indexOfFirst { it.id == toKey }
|
||||
val insertIndex = if (fromIndex < toIndex) newToIndex + 1 else newToIndex
|
||||
|
||||
newList.add(insertIndex, movedItem)
|
||||
|
||||
var actualSection = movedItem.section
|
||||
for (i in insertIndex downTo 0) {
|
||||
val item = newList[i]
|
||||
if (item.type == PdfFlatItemType.SECTION_HEADER) {
|
||||
actualSection = item.section
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
newList[insertIndex] = movedItem.copy(section = actualSection)
|
||||
flatItems = newList
|
||||
}
|
||||
}
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = onDismiss,
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false)
|
||||
) {
|
||||
androidx.compose.material3.Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.windowInsetsPadding(WindowInsets.navigationBars),
|
||||
color = MaterialTheme.colorScheme.surface
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxSize().padding(horizontal = 20.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.title_customize_toolbar),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
IconButton(onClick = onDismiss) {
|
||||
Icon(Icons.Default.Close, contentDescription = stringResource(R.string.action_close))
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(bottom = 24.dp)
|
||||
) {
|
||||
items(flatItems, key = { it.id }) { item ->
|
||||
val isDragged = item.id == dragDropState.draggedItemId
|
||||
val zIndex = if (isDragged) 1f else 0f
|
||||
val elevation = if (isDragged) 8.dp else 0.dp
|
||||
val scale = if (isDragged) 1.03f else 1f
|
||||
val translationY = if (isDragged) dragDropState.dragOffset.y else 0f
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.clickable {
|
||||
val newSet = hiddenTools.toMutableSet()
|
||||
if (newSet.contains(tool.name)) newSet.remove(tool.name)
|
||||
else newSet.add(tool.name)
|
||||
onUpdate(newSet)
|
||||
.then(if (isDragged) Modifier else Modifier.animateItem())
|
||||
.zIndex(zIndex)
|
||||
.graphicsLayer {
|
||||
this.translationY = translationY
|
||||
this.scaleX = scale
|
||||
this.scaleY = scale
|
||||
this.shadowElevation = elevation.toPx()
|
||||
}
|
||||
.padding(vertical = 12.dp, horizontal = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = tool.title,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Switch(
|
||||
checked = !hiddenTools.contains(tool.name),
|
||||
onCheckedChange = { isVisible ->
|
||||
val newSet = hiddenTools.toMutableSet()
|
||||
if (isVisible) newSet.remove(tool.name) else newSet.add(tool.name)
|
||||
onUpdate(newSet)
|
||||
when (item.type) {
|
||||
PdfFlatItemType.SECTION_HEADER -> {
|
||||
Text(
|
||||
text = item.title ?: "",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.padding(top = 16.dp, bottom = 8.dp, start = 4.dp)
|
||||
)
|
||||
}
|
||||
)
|
||||
PdfFlatItemType.EMPTY_PLACEHOLDER -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(64.dp)
|
||||
.padding(vertical = 4.dp)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f), RoundedCornerShape(12.dp)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("Drop tools here", color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
}
|
||||
PdfFlatItemType.TOOL -> {
|
||||
PdfToolbarDragRow(
|
||||
tool = item.tool!!,
|
||||
isDragging = isDragged,
|
||||
onDragStart = { dragDropState.onDragStart(item.id) },
|
||||
onDrag = { dragDropState.onDrag(it) },
|
||||
onDragEnd = {
|
||||
dragDropState.onDragEnd()
|
||||
flatItems = sanitizePdfPlaceholders(flatItems).toList()
|
||||
commitDragDrop()
|
||||
}
|
||||
)
|
||||
}
|
||||
PdfFlatItemType.MORE_HEADER -> {
|
||||
Text(
|
||||
text = item.title ?: "More menu",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(top = 24.dp, bottom = 8.dp, start = 4.dp)
|
||||
)
|
||||
}
|
||||
PdfFlatItemType.MORE_TOOL -> {
|
||||
PdfMoreToolVisibilityRow(
|
||||
title = item.tool!!.title,
|
||||
visible = !localHiddenTools.contains(item.tool.name),
|
||||
onToggle = {
|
||||
localHiddenTools = if (localHiddenTools.contains(item.tool.name)) {
|
||||
localHiddenTools - item.tool.name
|
||||
} else {
|
||||
localHiddenTools + item.tool.name
|
||||
}
|
||||
onUpdate(localHiddenTools)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -107,6 +360,154 @@ fun PdfCustomizeToolsSheet(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PdfToolbarDragRow(
|
||||
tool: PdfReaderTool,
|
||||
isDragging: Boolean,
|
||||
onDragStart: () -> Unit,
|
||||
onDrag: (Offset) -> Unit,
|
||||
onDragEnd: () -> Unit
|
||||
) {
|
||||
androidx.compose.material3.Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = if (isDragging) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(start = 16.dp, top = 8.dp, bottom = 8.dp, end = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
PdfToolPreviewIcon(tool)
|
||||
Spacer(Modifier.width(16.dp))
|
||||
Text(
|
||||
text = tool.title,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Icon(
|
||||
Icons.Default.Menu,
|
||||
contentDescription = "Drag to reorder",
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.padding(12.dp)
|
||||
.pointerInput(tool) {
|
||||
detectDragGestures(
|
||||
onDragStart = { onDragStart() },
|
||||
onDrag = { change, dragAmount ->
|
||||
change.consume()
|
||||
onDrag(dragAmount)
|
||||
},
|
||||
onDragEnd = onDragEnd,
|
||||
onDragCancel = onDragEnd
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PdfToolbarDragRow(
|
||||
tool: PdfReaderTool,
|
||||
isDragging: Boolean,
|
||||
onBounds: (Rect) -> Unit,
|
||||
onDragStart: (Offset) -> Unit,
|
||||
onDrag: (Offset) -> Unit,
|
||||
onDragEnd: () -> Unit
|
||||
) {
|
||||
var bounds by remember { mutableStateOf<Rect?>(null) }
|
||||
androidx.compose.material3.Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp)
|
||||
.onGloballyPositioned {
|
||||
bounds = it.boundsInWindow()
|
||||
onBounds(it.boundsInWindow())
|
||||
}
|
||||
.pointerInput(tool) {
|
||||
detectDragGesturesAfterLongPress(
|
||||
onDragStart = { onDragStart(bounds?.center ?: Offset.Zero) },
|
||||
onDragEnd = onDragEnd,
|
||||
onDragCancel = onDragEnd,
|
||||
onDrag = { change, dragAmount ->
|
||||
change.consume()
|
||||
onDrag(dragAmount)
|
||||
}
|
||||
)
|
||||
},
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = if (isDragging) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surface
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
PdfToolPreviewIcon(tool)
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Text(
|
||||
text = tool.title,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Icon(Icons.Default.Menu, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PdfMoreToolVisibilityRow(
|
||||
title: String,
|
||||
visible: Boolean,
|
||||
onToggle: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.clickable(onClick = onToggle)
|
||||
.padding(vertical = 12.dp, horizontal = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
if (visible) {
|
||||
Icon(Icons.Default.Check, contentDescription = null, tint = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class PdfToolbarSection(val title: String) {
|
||||
TOP("Top Bar"),
|
||||
BOTTOM("Bottom Bar"),
|
||||
HIDDEN("Hidden Tools")
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PdfToolPreviewIcon(tool: PdfReaderTool) {
|
||||
when (tool) {
|
||||
PdfReaderTool.DICTIONARY -> Icon(painterResource(id = R.drawable.dictionary), contentDescription = tool.title, modifier = Modifier.size(20.dp))
|
||||
PdfReaderTool.THEME -> Icon(painterResource(id = R.drawable.palette), contentDescription = tool.title, modifier = Modifier.size(20.dp))
|
||||
PdfReaderTool.LOCK_PANNING -> Icon(Icons.Default.LockOpen, contentDescription = tool.title, modifier = Modifier.size(20.dp))
|
||||
PdfReaderTool.SLIDER -> Icon(painterResource(id = R.drawable.slider), contentDescription = tool.title, modifier = Modifier.size(20.dp))
|
||||
PdfReaderTool.TOC -> Icon(Icons.Default.Menu, contentDescription = tool.title, modifier = Modifier.size(20.dp))
|
||||
PdfReaderTool.SEARCH -> Icon(Icons.Default.Search, contentDescription = tool.title, modifier = Modifier.size(20.dp))
|
||||
PdfReaderTool.HIGHLIGHT_ALL -> Icon(painterResource(id = R.drawable.highlight_text), contentDescription = tool.title, modifier = Modifier.size(20.dp))
|
||||
PdfReaderTool.AI_FEATURES -> Icon(painterResource(id = R.drawable.ai), contentDescription = tool.title, modifier = Modifier.size(20.dp))
|
||||
PdfReaderTool.EDIT_MODE -> Icon(Icons.Default.Edit, contentDescription = tool.title, modifier = Modifier.size(20.dp))
|
||||
PdfReaderTool.TTS_CONTROLS -> Icon(painterResource(id = R.drawable.text_to_speech), contentDescription = tool.title, modifier = Modifier.size(20.dp))
|
||||
else -> Icon(Icons.Default.MoreVert, contentDescription = tool.title, modifier = Modifier.size(20.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PdfVisualOptionsSheet(
|
||||
systemUiMode: SystemUiMode,
|
||||
|
|
@ -150,4 +551,4 @@ fun PdfVisualOptionsSheet(
|
|||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,9 @@ object PdfToHtmlGenerator {
|
|||
}
|
||||
|
||||
try {
|
||||
val doc = pdfiumCore.newDocument(pfd)
|
||||
val doc = PdfiumEngineProvider.withPdfium {
|
||||
pdfiumCore.newDocument(pfd)
|
||||
}
|
||||
val totalPages = doc.getPageCount()
|
||||
Timber.tag(TAG).d("Document loaded. Total pages: $totalPages")
|
||||
|
||||
|
|
@ -56,7 +58,9 @@ object PdfToHtmlGenerator {
|
|||
writer.write(buildGlobalHtmlFooter())
|
||||
}
|
||||
|
||||
doc.close()
|
||||
PdfiumEngineProvider.withPdfium {
|
||||
doc.close()
|
||||
}
|
||||
pfd.close()
|
||||
Timber.tag(TAG).d("generateHtmlFile SUCCESS | ${System.currentTimeMillis() - t0}ms")
|
||||
return@withContext true
|
||||
|
|
@ -137,14 +141,14 @@ object PdfToHtmlGenerator {
|
|||
val textPagePtr = getNativePointer(textPage)
|
||||
|
||||
val imageElements = mutableListOf<ImageElement>()
|
||||
val objCount = NativePdfiumBridge.getPageObjectCount(pagePtr)
|
||||
val objCount = PdfiumEngineProvider.bridge.getPageObjectCount(pagePtr)
|
||||
for (i in 0 until objCount) {
|
||||
if (NativePdfiumBridge.getPageObjectType(pagePtr, i) == 3) {
|
||||
if (PdfiumEngineProvider.bridge.getPageObjectType(pagePtr, i) == 3) {
|
||||
val bbox = FloatArray(4)
|
||||
if (NativePdfiumBridge.getPageObjectBoundingBox(pagePtr, i, bbox)) {
|
||||
if (PdfiumEngineProvider.bridge.getPageObjectBoundingBox(pagePtr, i, bbox)) {
|
||||
val topY = bbox[3]
|
||||
val dimens = IntArray(2)
|
||||
val pixels = NativePdfiumBridge.extractImagePixels(pagePtr, i, dimens)
|
||||
val pixels = PdfiumEngineProvider.bridge.extractImagePixels(pagePtr, i, dimens)
|
||||
if (pixels != null && dimens[0] > 0 && dimens[1] > 0) {
|
||||
try {
|
||||
val bmp = Bitmap.createBitmap(pixels, dimens[0], dimens[1], Bitmap.Config.ARGB_8888)
|
||||
|
|
@ -175,11 +179,11 @@ object PdfToHtmlGenerator {
|
|||
val flags: IntArray?
|
||||
val charBoxes: FloatArray?
|
||||
|
||||
synchronized(NativePdfiumBridge::class.java) {
|
||||
sizes = NativePdfiumBridge.getPageFontSizes(textPagePtr, actualCount)
|
||||
weights = NativePdfiumBridge.getPageFontWeights(textPagePtr, actualCount)
|
||||
flags = NativePdfiumBridge.getPageFontFlags(textPagePtr, actualCount)
|
||||
charBoxes = NativePdfiumBridge.getPageCharBoxes(textPagePtr, actualCount)
|
||||
synchronized(PdfiumEngineProvider.lock) {
|
||||
sizes = PdfiumEngineProvider.bridge.getPageFontSizes(textPagePtr, actualCount)
|
||||
weights = PdfiumEngineProvider.bridge.getPageFontWeights(textPagePtr, actualCount)
|
||||
flags = PdfiumEngineProvider.bridge.getPageFontFlags(textPagePtr, actualCount)
|
||||
charBoxes = PdfiumEngineProvider.bridge.getPageCharBoxes(textPagePtr, actualCount)
|
||||
}
|
||||
|
||||
if (sizes == null || weights == null || flags == null) {
|
||||
|
|
@ -547,4 +551,4 @@ object PdfToHtmlGenerator {
|
|||
}
|
||||
return 0L
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,13 +18,14 @@ import androidx.compose.foundation.rememberScrollState
|
|||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.Undo
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
|
|
@ -33,6 +34,7 @@ import androidx.compose.ui.text.font.FontWeight
|
|||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.aryan.reader.BuildConfig
|
||||
|
|
@ -41,9 +43,23 @@ import com.aryan.reader.R
|
|||
import com.aryan.reader.SearchState
|
||||
import com.aryan.reader.SearchTopBar
|
||||
import com.aryan.reader.TooltipIconButton
|
||||
import com.aryan.reader.areReaderAiFeaturesEnabled
|
||||
import com.aryan.reader.epubreader.SystemUiMode
|
||||
import kotlin.collections.isNotEmpty
|
||||
|
||||
private val pdfToolbarTools = setOf(
|
||||
PdfReaderTool.DICTIONARY,
|
||||
PdfReaderTool.THEME,
|
||||
PdfReaderTool.LOCK_PANNING,
|
||||
PdfReaderTool.SLIDER,
|
||||
PdfReaderTool.TOC,
|
||||
PdfReaderTool.SEARCH,
|
||||
PdfReaderTool.HIGHLIGHT_ALL,
|
||||
PdfReaderTool.AI_FEATURES,
|
||||
PdfReaderTool.EDIT_MODE,
|
||||
PdfReaderTool.TTS_CONTROLS
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
internal fun PdfTopBar(
|
||||
|
|
@ -60,6 +76,8 @@ internal fun PdfTopBar(
|
|||
totalPages: Int,
|
||||
pagerStatePageCount: Int,
|
||||
hiddenTools: Set<String>,
|
||||
toolOrder: List<PdfReaderTool>,
|
||||
bottomTools: Set<String>,
|
||||
isScrollLocked: Boolean,
|
||||
isEditMode: Boolean,
|
||||
displayMode: DisplayMode,
|
||||
|
|
@ -83,6 +101,16 @@ internal fun PdfTopBar(
|
|||
onShowCustomizeTools: () -> Unit,
|
||||
onShowOcrLanguage: () -> Unit,
|
||||
onShowVisualOptions: () -> Unit,
|
||||
onShowSlider: () -> Unit,
|
||||
onShowToc: () -> Unit,
|
||||
onSearchClick: () -> Unit,
|
||||
onToggleHighlights: () -> Unit,
|
||||
onShowAiHub: () -> Unit,
|
||||
onToggleEditMode: () -> Unit,
|
||||
onToggleTts: () -> Unit,
|
||||
isTtsPlayingOrLoading: Boolean,
|
||||
showAllTextHighlights: Boolean,
|
||||
isHighlightingLoading: Boolean,
|
||||
tapToNavigateEnabled: Boolean,
|
||||
onToggleTapToNavigate: () -> Unit,
|
||||
onChangeDisplayMode: (DisplayMode) -> Unit,
|
||||
|
|
@ -153,35 +181,89 @@ internal fun PdfTopBar(
|
|||
modifier = Modifier.padding(start = 12.dp).weight(1f).testTag("PageNumberIndicator")
|
||||
)
|
||||
|
||||
if (!hiddenTools.contains(PdfReaderTool.THEME.name)) {
|
||||
TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_theme),
|
||||
description = stringResource(R.string.tooltip_theme_desc),
|
||||
onClick = onShowThemePanel
|
||||
) {
|
||||
Icon(painterResource(id = R.drawable.palette), contentDescription = stringResource(R.string.tooltip_theme_desc), tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
toolOrder
|
||||
.filter { it in pdfToolbarTools && !bottomTools.contains(it.name) && !hiddenTools.contains(it.name) }
|
||||
.forEach { tool ->
|
||||
when (tool) {
|
||||
PdfReaderTool.THEME -> TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_theme),
|
||||
description = stringResource(R.string.tooltip_theme_desc),
|
||||
onClick = onShowThemePanel
|
||||
) {
|
||||
Icon(painterResource(id = R.drawable.palette), contentDescription = stringResource(R.string.tooltip_theme_desc), tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
PdfReaderTool.LOCK_PANNING -> TooltipIconButton(
|
||||
text = if (isScrollLocked) stringResource(R.string.tooltip_unlock_pan) else stringResource(R.string.tooltip_lock_pan),
|
||||
description = if (isScrollLocked) stringResource(R.string.tooltip_unlock_pan_desc) else stringResource(R.string.tooltip_lock_pan_desc),
|
||||
onClick = onToggleScrollLock
|
||||
) {
|
||||
Icon(if (isScrollLocked) Icons.Default.Lock else Icons.Default.LockOpen, contentDescription = if (isScrollLocked) stringResource(R.string.tooltip_unlock_pan) else stringResource(R.string.tooltip_lock_pan), tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
PdfReaderTool.DICTIONARY -> TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_dictionary),
|
||||
description = stringResource(R.string.tooltip_dictionary_desc),
|
||||
onClick = onShowDictionarySettings
|
||||
) {
|
||||
Icon(painterResource(id = R.drawable.dictionary), contentDescription = stringResource(R.string.content_desc_dictionary_settings), tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
PdfReaderTool.SLIDER -> TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_slider),
|
||||
description = stringResource(R.string.tooltip_slider_desc),
|
||||
onClick = onShowSlider,
|
||||
enabled = !isTtsPlayingOrLoading
|
||||
) {
|
||||
Icon(painterResource(id = R.drawable.slider), contentDescription = stringResource(R.string.content_desc_navigate_slider))
|
||||
}
|
||||
PdfReaderTool.TOC -> TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_toc),
|
||||
description = stringResource(R.string.tooltip_toc_desc),
|
||||
onClick = onShowToc,
|
||||
enabled = !isTtsPlayingOrLoading
|
||||
) {
|
||||
Icon(Icons.Default.Menu, contentDescription = stringResource(R.string.content_desc_table_of_contents))
|
||||
}
|
||||
PdfReaderTool.SEARCH -> TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_search),
|
||||
description = stringResource(R.string.tooltip_search_desc),
|
||||
onClick = onSearchClick,
|
||||
enabled = !isTtsPlayingOrLoading
|
||||
) {
|
||||
Icon(Icons.Default.Search, contentDescription = stringResource(R.string.action_search))
|
||||
}
|
||||
PdfReaderTool.HIGHLIGHT_ALL -> TooltipIconButton(
|
||||
text = if (showAllTextHighlights) stringResource(R.string.tooltip_highlights_off) else stringResource(R.string.tooltip_highlights),
|
||||
description = if (showAllTextHighlights) stringResource(R.string.tooltip_highlights_off_desc) else stringResource(R.string.tooltip_highlights_desc),
|
||||
onClick = onToggleHighlights
|
||||
) {
|
||||
if (isHighlightingLoading) CircularProgressIndicator(Modifier.size(24.dp))
|
||||
else Icon(painterResource(id = R.drawable.highlight_text), contentDescription = stringResource(R.string.content_desc_highlight_all_text), tint = if (showAllTextHighlights) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
PdfReaderTool.AI_FEATURES -> if (areReaderAiFeaturesEnabled(LocalContext.current)) {
|
||||
TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_ai),
|
||||
description = stringResource(R.string.tooltip_ai_desc),
|
||||
onClick = onShowAiHub
|
||||
) {
|
||||
Icon(painterResource(id = R.drawable.ai), contentDescription = stringResource(R.string.tooltip_ai))
|
||||
}
|
||||
}
|
||||
PdfReaderTool.EDIT_MODE -> TooltipIconButton(
|
||||
text = if (isEditMode) stringResource(R.string.tooltip_edit_mode_exit) else stringResource(R.string.tooltip_edit_mode),
|
||||
description = if (isEditMode) stringResource(R.string.tooltip_edit_mode_exit_desc) else stringResource(R.string.tooltip_edit_mode_desc),
|
||||
onClick = onToggleEditMode
|
||||
) {
|
||||
Icon(Icons.Default.Edit, contentDescription = stringResource(R.string.content_desc_toggle_editing_mode), tint = if (isEditMode) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
PdfReaderTool.TTS_CONTROLS -> TooltipIconButton(
|
||||
text = if (isTtsSessionActive) stringResource(R.string.tooltip_tts_stop) else stringResource(R.string.tooltip_tts_start),
|
||||
description = if (isTtsSessionActive) stringResource(R.string.tooltip_tts_stop_desc) else stringResource(R.string.tooltip_tts_start_desc),
|
||||
onClick = onToggleTts
|
||||
) {
|
||||
Icon(if (isTtsSessionActive) painterResource(id = R.drawable.close) else painterResource(id = R.drawable.text_to_speech), contentDescription = if (isTtsSessionActive) stringResource(R.string.content_desc_stop_tts) else stringResource(R.string.content_desc_start_tts), tint = if (isTtsSessionActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hiddenTools.contains(PdfReaderTool.LOCK_PANNING.name)) {
|
||||
TooltipIconButton(
|
||||
text = if (isScrollLocked) stringResource(R.string.tooltip_unlock_pan) else stringResource(R.string.tooltip_lock_pan),
|
||||
description = if (isScrollLocked) stringResource(R.string.tooltip_unlock_pan_desc) else stringResource(R.string.tooltip_lock_pan_desc),
|
||||
onClick = onToggleScrollLock
|
||||
) {
|
||||
Icon(if (isScrollLocked) Icons.Default.Lock else Icons.Default.LockOpen, contentDescription = if (isScrollLocked) stringResource(R.string.tooltip_unlock_pan) else stringResource(R.string.tooltip_lock_pan), tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
}
|
||||
|
||||
if (!hiddenTools.contains(PdfReaderTool.DICTIONARY.name)) {
|
||||
TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_dictionary),
|
||||
description = stringResource(R.string.tooltip_dictionary_desc),
|
||||
onClick = onShowDictionarySettings
|
||||
) {
|
||||
Icon(painterResource(id = R.drawable.dictionary), contentDescription = stringResource(R.string.content_desc_dictionary_settings), tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
}
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
TooltipIconButton(text = stringResource(R.string.tooltip_demo_annotations), onClick = onGenerateDemoAnnotations) {
|
||||
|
|
@ -197,14 +279,25 @@ internal fun PdfTopBar(
|
|||
|
||||
Box {
|
||||
var showMoreMenu by remember { mutableStateOf(false) }
|
||||
var showHiddenToolsExpanded by remember { mutableStateOf(false) }
|
||||
TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_more_options),
|
||||
description = stringResource(R.string.tooltip_more_options_desc),
|
||||
onClick = { showMoreMenu = true }) {
|
||||
onClick = {
|
||||
showHiddenToolsExpanded = false
|
||||
showMoreMenu = true
|
||||
}) {
|
||||
Icon(Icons.Default.MoreVert, contentDescription = stringResource(R.string.tooltip_more_options))
|
||||
}
|
||||
|
||||
DropdownMenu(expanded = showMoreMenu, onDismissRequest = { showMoreMenu = false }) {
|
||||
DropdownMenu(
|
||||
expanded = showMoreMenu,
|
||||
onDismissRequest = {
|
||||
showHiddenToolsExpanded = false
|
||||
showMoreMenu = false
|
||||
}
|
||||
) {
|
||||
val hiddenToolbarTools = toolOrder.filter { it in pdfToolbarTools && hiddenTools.contains(it.name) }
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.title_customize_toolbar)) },
|
||||
onClick = { showMoreMenu = false; onShowCustomizeTools() },
|
||||
|
|
@ -212,6 +305,47 @@ internal fun PdfTopBar(
|
|||
)
|
||||
HorizontalDivider()
|
||||
|
||||
if (hiddenToolbarTools.isNotEmpty()) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Hidden tools") },
|
||||
onClick = { showHiddenToolsExpanded = !showHiddenToolsExpanded },
|
||||
trailingIcon = {
|
||||
Icon(
|
||||
Icons.Default.ArrowDropDown,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.rotate(if (showHiddenToolsExpanded) 180f else 0f)
|
||||
)
|
||||
}
|
||||
)
|
||||
if (showHiddenToolsExpanded) {
|
||||
hiddenToolbarTools.forEach { tool ->
|
||||
HiddenPdfToolMenuItem(
|
||||
tool = tool,
|
||||
isTtsPlayingOrLoading = isTtsPlayingOrLoading,
|
||||
showAllTextHighlights = showAllTextHighlights,
|
||||
isHighlightingLoading = isHighlightingLoading,
|
||||
isEditMode = isEditMode,
|
||||
isTtsSessionActive = isTtsSessionActive,
|
||||
closeMenu = {
|
||||
showHiddenToolsExpanded = false
|
||||
showMoreMenu = false
|
||||
},
|
||||
onShowThemePanel = onShowThemePanel,
|
||||
onToggleScrollLock = onToggleScrollLock,
|
||||
onShowDictionarySettings = onShowDictionarySettings,
|
||||
onShowSlider = onShowSlider,
|
||||
onShowToc = onShowToc,
|
||||
onSearchClick = onSearchClick,
|
||||
onToggleHighlights = onToggleHighlights,
|
||||
onShowAiHub = onShowAiHub,
|
||||
onToggleEditMode = onToggleEditMode,
|
||||
onToggleTts = onToggleTts
|
||||
)
|
||||
}
|
||||
}
|
||||
HorizontalDivider()
|
||||
}
|
||||
|
||||
if (BuildConfig.IS_PRO && !hiddenTools.contains(PdfReaderTool.OCR_LANGUAGE.name)) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.menu_ocr_language)) },
|
||||
|
|
@ -402,6 +536,72 @@ internal fun PdfTopBar(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HiddenPdfToolMenuItem(
|
||||
tool: PdfReaderTool,
|
||||
isTtsPlayingOrLoading: Boolean,
|
||||
showAllTextHighlights: Boolean,
|
||||
isHighlightingLoading: Boolean,
|
||||
isEditMode: Boolean,
|
||||
isTtsSessionActive: Boolean,
|
||||
closeMenu: () -> Unit,
|
||||
onShowThemePanel: () -> Unit,
|
||||
onToggleScrollLock: () -> Unit,
|
||||
onShowDictionarySettings: () -> Unit,
|
||||
onShowSlider: () -> Unit,
|
||||
onShowToc: () -> Unit,
|
||||
onSearchClick: () -> Unit,
|
||||
onToggleHighlights: () -> Unit,
|
||||
onShowAiHub: () -> Unit,
|
||||
onToggleEditMode: () -> Unit,
|
||||
onToggleTts: () -> Unit
|
||||
) {
|
||||
val enabled = when (tool) {
|
||||
PdfReaderTool.SLIDER,
|
||||
PdfReaderTool.TOC,
|
||||
PdfReaderTool.SEARCH -> !isTtsPlayingOrLoading
|
||||
else -> true
|
||||
}
|
||||
DropdownMenuItem(
|
||||
text = { Text(tool.title) },
|
||||
enabled = enabled,
|
||||
onClick = {
|
||||
closeMenu()
|
||||
when (tool) {
|
||||
PdfReaderTool.THEME -> onShowThemePanel()
|
||||
PdfReaderTool.LOCK_PANNING -> onToggleScrollLock()
|
||||
PdfReaderTool.DICTIONARY -> onShowDictionarySettings()
|
||||
PdfReaderTool.SLIDER -> onShowSlider()
|
||||
PdfReaderTool.TOC -> onShowToc()
|
||||
PdfReaderTool.SEARCH -> onSearchClick()
|
||||
PdfReaderTool.HIGHLIGHT_ALL -> onToggleHighlights()
|
||||
PdfReaderTool.AI_FEATURES -> onShowAiHub()
|
||||
PdfReaderTool.EDIT_MODE -> onToggleEditMode()
|
||||
PdfReaderTool.TTS_CONTROLS -> onToggleTts()
|
||||
else -> Unit
|
||||
}
|
||||
},
|
||||
leadingIcon = {
|
||||
when (tool) {
|
||||
PdfReaderTool.DICTIONARY -> Icon(painterResource(id = R.drawable.dictionary), contentDescription = null, modifier = Modifier.size(20.dp))
|
||||
PdfReaderTool.THEME -> Icon(painterResource(id = R.drawable.palette), contentDescription = null, modifier = Modifier.size(20.dp))
|
||||
PdfReaderTool.LOCK_PANNING -> Icon(Icons.Default.LockOpen, contentDescription = null, modifier = Modifier.size(20.dp))
|
||||
PdfReaderTool.SLIDER -> Icon(painterResource(id = R.drawable.slider), contentDescription = null, modifier = Modifier.size(20.dp))
|
||||
PdfReaderTool.TOC -> Icon(Icons.Default.Menu, contentDescription = null, modifier = Modifier.size(20.dp))
|
||||
PdfReaderTool.SEARCH -> Icon(Icons.Default.Search, contentDescription = null, modifier = Modifier.size(20.dp))
|
||||
PdfReaderTool.HIGHLIGHT_ALL -> {
|
||||
if (isHighlightingLoading) CircularProgressIndicator(Modifier.size(20.dp))
|
||||
else Icon(painterResource(id = R.drawable.highlight_text), contentDescription = null, modifier = Modifier.size(20.dp), tint = if (showAllTextHighlights) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
PdfReaderTool.AI_FEATURES -> Icon(painterResource(id = R.drawable.ai), contentDescription = null, modifier = Modifier.size(20.dp))
|
||||
PdfReaderTool.EDIT_MODE -> Icon(Icons.Default.Edit, contentDescription = null, modifier = Modifier.size(20.dp), tint = if (isEditMode) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
PdfReaderTool.TTS_CONTROLS -> Icon(if (isTtsSessionActive) painterResource(id = R.drawable.close) else painterResource(id = R.drawable.text_to_speech), contentDescription = null, modifier = Modifier.size(20.dp), tint = if (isTtsSessionActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
else -> Icon(Icons.Default.MoreVert, contentDescription = null, modifier = Modifier.size(20.dp))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ReflowProgressOverlay(
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -447,6 +647,89 @@ fun ReflowProgressOverlay(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PdfJumpHistoryBar(
|
||||
modifier: Modifier = Modifier,
|
||||
showStandardBars: Boolean,
|
||||
searchStateActive: Boolean,
|
||||
backPage: Int?,
|
||||
forwardPage: Int?,
|
||||
onBack: () -> Unit,
|
||||
onForward: () -> Unit,
|
||||
onClear: () -> Unit
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = showStandardBars && !searchStateActive && (backPage != null || forwardPage != null),
|
||||
enter = slideInVertically(animationSpec = tween(200)) { fullHeight -> fullHeight } + fadeIn(animationSpec = tween(200)),
|
||||
exit = slideOutVertically(animationSpec = tween(200)) { fullHeight -> fullHeight } + fadeOut(animationSpec = tween(200)),
|
||||
modifier = modifier
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = MaterialTheme.colorScheme.surfaceContainer,
|
||||
tonalElevation = 3.dp
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(40.dp)
|
||||
.padding(horizontal = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
TextButton(
|
||||
onClick = onBack,
|
||||
enabled = backPage != null,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = stringResource(R.string.content_desc_jump_back),
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text(
|
||||
text = backPage?.let { stringResource(R.string.pdf_page_short, it + 1) } ?: "",
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
|
||||
TextButton(
|
||||
onClick = onClear,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Close,
|
||||
contentDescription = stringResource(R.string.action_clear),
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text(stringResource(R.string.action_clear), maxLines = 1)
|
||||
}
|
||||
|
||||
TextButton(
|
||||
onClick = onForward,
|
||||
enabled = forwardPage != null,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = forwardPage?.let { stringResource(R.string.pdf_page_short, it + 1) } ?: "",
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.ArrowForward,
|
||||
contentDescription = stringResource(R.string.content_desc_jump_forward),
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PdfBottomBar(
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -455,14 +738,17 @@ fun PdfBottomBar(
|
|||
systemUiMode: SystemUiMode,
|
||||
navBarHeightDp: Dp,
|
||||
hiddenTools: Set<String>,
|
||||
toolOrder: List<PdfReaderTool>,
|
||||
bottomTools: Set<String>,
|
||||
isTtsPlayingOrLoading: Boolean,
|
||||
showAllTextHighlights: Boolean,
|
||||
isHighlightingLoading: Boolean,
|
||||
isEditMode: Boolean,
|
||||
isTtsSessionActive: Boolean,
|
||||
ttsErrorMessage: String?,
|
||||
jumpBackPage: Int?,
|
||||
onJumpBack: () -> Unit,
|
||||
onShowThemePanel: () -> Unit,
|
||||
onToggleScrollLock: () -> Unit,
|
||||
onShowDictionarySettings: () -> Unit,
|
||||
onShowSlider: () -> Unit,
|
||||
onShowToc: () -> Unit,
|
||||
onSearchClick: () -> Unit,
|
||||
|
|
@ -490,106 +776,91 @@ fun PdfBottomBar(
|
|||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
if (jumpBackPage != null) {
|
||||
TooltipIconButton(
|
||||
text = stringResource(R.string.action_jump_back_to_page, jumpBackPage + 1),
|
||||
description = stringResource(R.string.desc_return_to_previous_page),
|
||||
onClick = onJumpBack
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.Undo,
|
||||
contentDescription = stringResource(R.string.content_desc_jump_back),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Text(
|
||||
text = "${jumpBackPage + 1}",
|
||||
fontSize = 10.sp,
|
||||
lineHeight = 10.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
toolOrder
|
||||
.filter { it in pdfToolbarTools && bottomTools.contains(it.name) && !hiddenTools.contains(it.name) }
|
||||
.forEach { tool ->
|
||||
when (tool) {
|
||||
PdfReaderTool.THEME -> TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_theme),
|
||||
description = stringResource(R.string.tooltip_theme_desc),
|
||||
onClick = onShowThemePanel
|
||||
) {
|
||||
Icon(painterResource(id = R.drawable.palette), contentDescription = stringResource(R.string.tooltip_theme_desc), tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
PdfReaderTool.LOCK_PANNING -> TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_lock_pan),
|
||||
description = stringResource(R.string.tooltip_lock_pan_desc),
|
||||
onClick = onToggleScrollLock
|
||||
) {
|
||||
Icon(Icons.Default.LockOpen, contentDescription = stringResource(R.string.tooltip_lock_pan), tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
PdfReaderTool.DICTIONARY -> TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_dictionary),
|
||||
description = stringResource(R.string.tooltip_dictionary_desc),
|
||||
onClick = onShowDictionarySettings
|
||||
) {
|
||||
Icon(painterResource(id = R.drawable.dictionary), contentDescription = stringResource(R.string.content_desc_dictionary_settings), tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
PdfReaderTool.SLIDER -> TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_slider),
|
||||
description = stringResource(R.string.tooltip_slider_desc),
|
||||
onClick = onShowSlider,
|
||||
enabled = !isTtsPlayingOrLoading
|
||||
) {
|
||||
Icon(painterResource(id = R.drawable.slider), contentDescription = stringResource(R.string.content_desc_navigate_slider))
|
||||
}
|
||||
PdfReaderTool.TOC -> TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_toc),
|
||||
description = stringResource(R.string.tooltip_toc_desc),
|
||||
onClick = onShowToc,
|
||||
enabled = !isTtsPlayingOrLoading,
|
||||
modifier = Modifier.testTag("TocButton")
|
||||
) {
|
||||
Icon(Icons.Default.Menu, contentDescription = stringResource(R.string.content_desc_table_of_contents))
|
||||
}
|
||||
PdfReaderTool.SEARCH -> TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_search),
|
||||
description = stringResource(R.string.tooltip_search_desc),
|
||||
onClick = onSearchClick,
|
||||
enabled = !isTtsPlayingOrLoading,
|
||||
modifier = Modifier.testTag("SearchButton")
|
||||
) {
|
||||
Icon(Icons.Default.Search, contentDescription = stringResource(R.string.action_search))
|
||||
}
|
||||
PdfReaderTool.HIGHLIGHT_ALL -> TooltipIconButton(
|
||||
text = if (showAllTextHighlights) stringResource(R.string.tooltip_highlights_off) else stringResource(R.string.tooltip_highlights),
|
||||
description = if (showAllTextHighlights) stringResource(R.string.tooltip_highlights_off_desc) else stringResource(R.string.tooltip_highlights_desc),
|
||||
onClick = onToggleHighlights
|
||||
) {
|
||||
if (isHighlightingLoading) CircularProgressIndicator(Modifier.size(24.dp))
|
||||
else Icon(painterResource(id = R.drawable.highlight_text), contentDescription = stringResource(R.string.content_desc_highlight_all_text), tint = if (showAllTextHighlights) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
PdfReaderTool.AI_FEATURES -> if (areReaderAiFeaturesEnabled(LocalContext.current)) {
|
||||
TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_ai),
|
||||
description = stringResource(R.string.tooltip_ai_desc),
|
||||
onClick = onShowAiHub
|
||||
) {
|
||||
Icon(painterResource(id = R.drawable.ai), contentDescription = stringResource(R.string.tooltip_ai))
|
||||
}
|
||||
}
|
||||
PdfReaderTool.EDIT_MODE -> TooltipIconButton(
|
||||
text = if (isEditMode) stringResource(R.string.tooltip_edit_mode_exit) else stringResource(R.string.tooltip_edit_mode),
|
||||
description = if (isEditMode) stringResource(R.string.tooltip_edit_mode_exit_desc) else stringResource(R.string.tooltip_edit_mode_desc),
|
||||
onClick = onToggleEditMode
|
||||
) {
|
||||
Icon(Icons.Default.Edit, contentDescription = stringResource(R.string.content_desc_toggle_editing_mode), tint = if (isEditMode) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
PdfReaderTool.TTS_CONTROLS -> TooltipIconButton(
|
||||
text = if (isTtsSessionActive) stringResource(R.string.tooltip_tts_stop) else stringResource(R.string.tooltip_tts_start),
|
||||
description = if (isTtsSessionActive) stringResource(R.string.tooltip_tts_stop_desc) else stringResource(R.string.tooltip_tts_start_desc),
|
||||
onClick = onToggleTts
|
||||
) {
|
||||
Icon(if (isTtsSessionActive) painterResource(id = R.drawable.close) else painterResource(id = R.drawable.text_to_speech), contentDescription = if (isTtsSessionActive) stringResource(R.string.content_desc_stop_tts) else stringResource(R.string.content_desc_start_tts), tint = if (isTtsSessionActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hiddenTools.contains(PdfReaderTool.SLIDER.name)) {
|
||||
TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_slider),
|
||||
description = stringResource(R.string.tooltip_slider_desc),
|
||||
onClick = onShowSlider,
|
||||
enabled = !isTtsPlayingOrLoading
|
||||
) {
|
||||
Icon(painterResource(id = R.drawable.slider), contentDescription = stringResource(R.string.content_desc_navigate_slider))
|
||||
}
|
||||
}
|
||||
if (!hiddenTools.contains(PdfReaderTool.TOC.name)) {
|
||||
TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_toc),
|
||||
description = stringResource(R.string.tooltip_toc_desc),
|
||||
onClick = onShowToc,
|
||||
enabled = !isTtsPlayingOrLoading,
|
||||
modifier = Modifier.testTag("TocButton")
|
||||
) {
|
||||
Icon(Icons.Default.Menu, contentDescription = stringResource(R.string.content_desc_table_of_contents))
|
||||
}
|
||||
}
|
||||
|
||||
if (!hiddenTools.contains(PdfReaderTool.SEARCH.name)) {
|
||||
TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_search),
|
||||
description = stringResource(R.string.tooltip_search_desc),
|
||||
onClick = onSearchClick,
|
||||
enabled = !isTtsPlayingOrLoading,
|
||||
modifier = Modifier.testTag("SearchButton")
|
||||
) {
|
||||
Icon(Icons.Default.Search, contentDescription = stringResource(R.string.action_search))
|
||||
}
|
||||
}
|
||||
if (!hiddenTools.contains(PdfReaderTool.HIGHLIGHT_ALL.name)) {
|
||||
TooltipIconButton(
|
||||
text = if (showAllTextHighlights) stringResource(R.string.tooltip_highlights_off) else stringResource(R.string.tooltip_highlights),
|
||||
description = if (showAllTextHighlights) stringResource(R.string.tooltip_highlights_off_desc) else stringResource(R.string.tooltip_highlights_desc),
|
||||
onClick = onToggleHighlights
|
||||
) {
|
||||
if (isHighlightingLoading) CircularProgressIndicator(Modifier.size(24.dp))
|
||||
else Icon(painterResource(id = R.drawable.highlight_text), contentDescription = stringResource(R.string.content_desc_highlight_all_text), tint = if (showAllTextHighlights) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
}
|
||||
|
||||
if (BuildConfig.FLAVOR != "oss" && !hiddenTools.contains(PdfReaderTool.AI_FEATURES.name)) {
|
||||
TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_ai),
|
||||
description = stringResource(R.string.tooltip_ai_desc),
|
||||
onClick = onShowAiHub
|
||||
) {
|
||||
Icon(painterResource(id = R.drawable.ai), contentDescription = stringResource(R.string.tooltip_ai))
|
||||
}
|
||||
}
|
||||
|
||||
if (!hiddenTools.contains(PdfReaderTool.EDIT_MODE.name)) {
|
||||
TooltipIconButton(
|
||||
text = if (isEditMode) stringResource(R.string.tooltip_edit_mode_exit) else stringResource(R.string.tooltip_edit_mode),
|
||||
description = if (isEditMode) stringResource(R.string.tooltip_edit_mode_exit_desc) else stringResource(R.string.tooltip_edit_mode_desc),
|
||||
onClick = onToggleEditMode
|
||||
) {
|
||||
Icon(Icons.Default.Edit, contentDescription = stringResource(R.string.content_desc_toggle_editing_mode), tint = if (isEditMode) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
}
|
||||
|
||||
if (!hiddenTools.contains(PdfReaderTool.TTS_CONTROLS.name)) {
|
||||
TooltipIconButton(
|
||||
text = if (isTtsSessionActive) stringResource(R.string.tooltip_tts_stop) else stringResource(R.string.tooltip_tts_start),
|
||||
description = if (isTtsSessionActive) stringResource(R.string.tooltip_tts_stop_desc) else stringResource(R.string.tooltip_tts_start_desc),
|
||||
onClick = onToggleTts
|
||||
) {
|
||||
Icon(if (isTtsSessionActive) painterResource(id = R.drawable.close) else painterResource(id = R.drawable.text_to_speech), contentDescription = if (isTtsSessionActive) stringResource(R.string.content_desc_stop_tts) else stringResource(R.string.content_desc_start_tts), tint = if (isTtsSessionActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
}
|
||||
|
||||
if (BuildConfig.FLAVOR != "oss") {
|
||||
TooltipIconButton(
|
||||
|
|
|
|||
|
|
@ -193,6 +193,7 @@ internal fun PdfVerticalReader(
|
|||
state: VerticalPdfReaderState,
|
||||
pdfDocument: StableHolder<ReaderDocument>,
|
||||
activeTheme: com.aryan.reader.ReaderTheme,
|
||||
activeTextureAlpha: Float = 0.55f,
|
||||
excludeImages: Boolean = false,
|
||||
totalPages: Int,
|
||||
virtualPages: List<VirtualPage> = emptyList(),
|
||||
|
|
@ -988,7 +989,7 @@ internal fun PdfVerticalReader(
|
|||
}
|
||||
|
||||
val buttons = currentEvent.buttons
|
||||
Timber.tag("StylusEraserDiagnostic").d(
|
||||
Timber.tag("StylusDebug").d(
|
||||
"VerticalReader | Type: ${down.type} | isPrimary: ${buttons.isPrimaryPressed} | isSecondary: ${buttons.isSecondaryPressed} | isTertiary: ${buttons.isTertiaryPressed} | buttonsString: $buttons"
|
||||
)
|
||||
|
||||
|
|
@ -1688,6 +1689,7 @@ internal fun PdfVerticalReader(
|
|||
virtualPage = virtualPage,
|
||||
totalPages = totalPages,
|
||||
activeTheme = activeTheme,
|
||||
activeTextureAlpha = activeTextureAlpha,
|
||||
excludeImages = excludeImages,
|
||||
externalScale = highResScale,
|
||||
onScaleChanged = {},
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
120
app/src/main/java/com/aryan/reader/pdf/PdfiumEngineProvider.kt
Normal file
120
app/src/main/java/com/aryan/reader/pdf/PdfiumEngineProvider.kt
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
package com.aryan.reader.pdf
|
||||
|
||||
import com.aryan.reader.shared.pdf.PdfiumBridge
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
internal object PdfiumEngineProvider {
|
||||
private val pdfiumMutex = Mutex()
|
||||
|
||||
val bridge: PdfiumBridge
|
||||
get() = AndroidPdfiumBridge
|
||||
|
||||
val lock: Any = this
|
||||
|
||||
suspend fun <T> withPdfium(block: suspend () -> T): T =
|
||||
pdfiumMutex.withLock { block() }
|
||||
|
||||
fun <T> withPdfiumBlocking(block: () -> T): T =
|
||||
runBlocking {
|
||||
pdfiumMutex.withLock { block() }
|
||||
}
|
||||
}
|
||||
|
||||
private object AndroidPdfiumBridge : PdfiumBridge {
|
||||
override fun getFontSize(textPagePtr: Long, index: Int): Double =
|
||||
PdfiumEngineProvider.withPdfiumBlocking {
|
||||
NativePdfiumBridge.getFontSize(textPagePtr, index)
|
||||
}
|
||||
|
||||
override fun getFontWeight(textPagePtr: Long, index: Int): Int =
|
||||
PdfiumEngineProvider.withPdfiumBlocking {
|
||||
NativePdfiumBridge.getFontWeight(textPagePtr, index)
|
||||
}
|
||||
|
||||
override fun getPageFontSizes(textPagePtr: Long, count: Int): FloatArray? =
|
||||
PdfiumEngineProvider.withPdfiumBlocking {
|
||||
NativePdfiumBridge.getPageFontSizes(textPagePtr, count)
|
||||
}
|
||||
|
||||
override fun getPageFontWeights(textPagePtr: Long, count: Int): IntArray? =
|
||||
PdfiumEngineProvider.withPdfiumBlocking {
|
||||
NativePdfiumBridge.getPageFontWeights(textPagePtr, count)
|
||||
}
|
||||
|
||||
override fun getPageFontFlags(textPagePtr: Long, count: Int): IntArray? =
|
||||
PdfiumEngineProvider.withPdfiumBlocking {
|
||||
NativePdfiumBridge.getPageFontFlags(textPagePtr, count)
|
||||
}
|
||||
|
||||
override fun getPageCharBoxes(textPagePtr: Long, count: Int): FloatArray? =
|
||||
PdfiumEngineProvider.withPdfiumBlocking {
|
||||
NativePdfiumBridge.getPageCharBoxes(textPagePtr, count)
|
||||
}
|
||||
|
||||
override fun getAnnotCount(pagePtr: Long): Int =
|
||||
PdfiumEngineProvider.withPdfiumBlocking {
|
||||
NativePdfiumBridge.getAnnotCount(pagePtr)
|
||||
}
|
||||
|
||||
override fun getAnnotSubtype(pagePtr: Long, index: Int): Int =
|
||||
PdfiumEngineProvider.withPdfiumBlocking {
|
||||
NativePdfiumBridge.getAnnotSubtype(pagePtr, index)
|
||||
}
|
||||
|
||||
override fun getAnnotRect(pagePtr: Long, index: Int): FloatArray? =
|
||||
PdfiumEngineProvider.withPdfiumBlocking {
|
||||
NativePdfiumBridge.getAnnotRect(pagePtr, index)
|
||||
}
|
||||
|
||||
override fun getAnnotString(pagePtr: Long, index: Int, key: String): String? =
|
||||
PdfiumEngineProvider.withPdfiumBlocking {
|
||||
NativePdfiumBridge.getAnnotString(pagePtr, index, key)
|
||||
}
|
||||
|
||||
override fun getPageObjectCount(pagePtr: Long): Int =
|
||||
PdfiumEngineProvider.withPdfiumBlocking {
|
||||
NativePdfiumBridge.getPageObjectCount(pagePtr)
|
||||
}
|
||||
|
||||
override fun getPageObjectType(pagePtr: Long, index: Int): Int =
|
||||
PdfiumEngineProvider.withPdfiumBlocking {
|
||||
NativePdfiumBridge.getPageObjectType(pagePtr, index)
|
||||
}
|
||||
|
||||
override fun getPageObjectBoundingBox(pagePtr: Long, index: Int, outRect: FloatArray): Boolean =
|
||||
PdfiumEngineProvider.withPdfiumBlocking {
|
||||
NativePdfiumBridge.getPageObjectBoundingBox(pagePtr, index, outRect)
|
||||
}
|
||||
|
||||
override fun extractImagePixels(pagePtr: Long, index: Int, dimens: IntArray): IntArray? =
|
||||
PdfiumEngineProvider.withPdfiumBlocking {
|
||||
NativePdfiumBridge.extractImagePixels(pagePtr, index, dimens)
|
||||
}
|
||||
|
||||
override fun performClick(pagePtr: Long, x: Double, y: Double): Boolean =
|
||||
PdfiumEngineProvider.withPdfiumBlocking {
|
||||
NativePdfiumBridge.performClick(pagePtr, x, y)
|
||||
}
|
||||
|
||||
override fun getLinkInfoAtPoint(docPtr: Long, pagePtr: Long, x: Double, y: Double): String? =
|
||||
PdfiumEngineProvider.withPdfiumBlocking {
|
||||
NativePdfiumBridge.getLinkInfoAtPoint(docPtr, pagePtr, x, y)
|
||||
}
|
||||
|
||||
override fun getAnnotSubtypeAtPoint(pagePtr: Long, x: Double, y: Double): Int =
|
||||
PdfiumEngineProvider.withPdfiumBlocking {
|
||||
NativePdfiumBridge.getAnnotSubtypeAtPoint(pagePtr, x, y)
|
||||
}
|
||||
|
||||
override fun getAnnotRectAtPoint(pagePtr: Long, x: Double, y: Double): FloatArray? =
|
||||
PdfiumEngineProvider.withPdfiumBlocking {
|
||||
NativePdfiumBridge.getAnnotRectAtPoint(pagePtr, x, y)
|
||||
}
|
||||
|
||||
override fun checkActionSupport(): Boolean =
|
||||
PdfiumEngineProvider.withPdfiumBlocking {
|
||||
NativePdfiumBridge.checkActionSupport()
|
||||
}
|
||||
}
|
||||
|
|
@ -28,6 +28,7 @@ import me.zhanghai.android.libarchive.ArchiveException
|
|||
import okhttp3.Request
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.zip.ZipFile
|
||||
import androidx.core.graphics.createBitmap
|
||||
|
||||
|
|
@ -88,42 +89,97 @@ object DocumentFactory {
|
|||
ArchiveDocumentWrapper(cacheFile)
|
||||
} else {
|
||||
val pfd = context.contentResolver.openFileDescriptor(uri, "r") ?: throw Exception("Failed to open PDF")
|
||||
PdfDocumentWrapper(pdfiumCore.newDocument(pfd, password))
|
||||
PdfDocumentWrapper(PdfiumEngineProvider.withPdfium { pdfiumCore.newDocument(pfd, password) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ================= PDF IMPLEMENTATION =================
|
||||
|
||||
private inline fun closePdfiumResource(tag: String, closeBlock: () -> Unit) {
|
||||
try {
|
||||
closeBlock()
|
||||
} catch (e: IllegalStateException) {
|
||||
if (e.message == "Already closed") {
|
||||
Timber.tag(tag).d(e, "Ignoring duplicate Pdfium close")
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PdfDocumentWrapper(val pdfDocument: PdfDocumentKt) : ReaderDocument {
|
||||
override suspend fun getPageCount() = pdfDocument.getPageCount()
|
||||
private val isClosed = AtomicBoolean(false)
|
||||
|
||||
override suspend fun getPageCount() = PdfiumEngineProvider.withPdfium {
|
||||
pdfDocument.getPageCount()
|
||||
}
|
||||
|
||||
override suspend fun openPage(pageIndex: Int): ReaderPage? {
|
||||
val page = pdfDocument.openPage(pageIndex) ?: return null
|
||||
if (isClosed.get()) return null
|
||||
val page = PdfiumEngineProvider.withPdfium {
|
||||
if (isClosed.get()) null else pdfDocument.openPage(pageIndex)
|
||||
} ?: return null
|
||||
return PdfPageWrapper(page)
|
||||
}
|
||||
override suspend fun getTableOfContents() = pdfDocument.getFixedTableOfContents()
|
||||
override fun close() { pdfDocument.close() }
|
||||
|
||||
override suspend fun getTableOfContents() = PdfiumEngineProvider.withPdfium {
|
||||
pdfDocument.getFixedTableOfContents()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
if (!isClosed.compareAndSet(false, true)) return
|
||||
PdfiumEngineProvider.withPdfiumBlocking {
|
||||
closePdfiumResource("PdfDocumentWrapper") { pdfDocument.close() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PdfPageWrapper(val pdfPage: PdfPageKt) : ReaderPage {
|
||||
override suspend fun getPageWidthPoint() = pdfPage.getPageWidthPoint()
|
||||
override suspend fun getPageHeightPoint() = pdfPage.getPageHeightPoint()
|
||||
override suspend fun getPageRotation() = pdfPage.getPageRotation()
|
||||
private val isClosed = AtomicBoolean(false)
|
||||
|
||||
override suspend fun getPageWidthPoint() = PdfiumEngineProvider.withPdfium {
|
||||
if (isClosed.get()) 0 else pdfPage.getPageWidthPoint()
|
||||
}
|
||||
|
||||
override suspend fun getPageHeightPoint() = PdfiumEngineProvider.withPdfium {
|
||||
if (isClosed.get()) 0 else pdfPage.getPageHeightPoint()
|
||||
}
|
||||
|
||||
override suspend fun getPageRotation() = PdfiumEngineProvider.withPdfium {
|
||||
if (isClosed.get()) 0 else pdfPage.getPageRotation()
|
||||
}
|
||||
|
||||
override suspend fun renderPageBitmap(bitmap: Bitmap, startX: Int, startY: Int, drawSizeX: Int, drawSizeY: Int, renderAnnot: Boolean) {
|
||||
pdfPage.renderPageBitmap(bitmap, startX, startY, drawSizeX, drawSizeY, renderAnnot)
|
||||
PdfiumEngineProvider.withPdfium {
|
||||
if (!isClosed.get()) {
|
||||
pdfPage.renderPageBitmap(bitmap, startX, startY, drawSizeX, drawSizeY, renderAnnot)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun mapRectToDevice(startX: Int, startY: Int, sizeX: Int, sizeY: Int, rotate: Int, coords: RectF) =
|
||||
pdfPage.mapRectToDevice(startX, startY, sizeX, sizeY, rotate, coords)
|
||||
PdfiumEngineProvider.withPdfium {
|
||||
if (isClosed.get()) Rect() else pdfPage.mapRectToDevice(startX, startY, sizeX, sizeY, rotate, coords)
|
||||
}
|
||||
|
||||
override suspend fun mapDeviceCoordsToPage(startX: Int, startY: Int, sizeX: Int, sizeY: Int, rotate: Int, deviceX: Int, deviceY: Int) =
|
||||
pdfPage.mapDeviceCoordsToPage(startX, startY, sizeX, sizeY, rotate, deviceX, deviceY)
|
||||
PdfiumEngineProvider.withPdfium {
|
||||
if (isClosed.get()) PointF() else pdfPage.mapDeviceCoordsToPage(startX, startY, sizeX, sizeY, rotate, deviceX, deviceY)
|
||||
}
|
||||
|
||||
override suspend fun openTextPage(): ReaderTextPage = PdfTextPageWrapper(pdfPage.openTextPage())
|
||||
override suspend fun openTextPage(): ReaderTextPage = PdfiumEngineProvider.withPdfium {
|
||||
if (isClosed.get()) DummyTextPage() else PdfTextPageWrapper(pdfPage.openTextPage())
|
||||
}
|
||||
|
||||
override suspend fun getLinks(): List<ReaderLink> {
|
||||
return pdfPage.getPageLinks().map { ReaderLink(it.uri, it.destPageIdx, it.bounds) }
|
||||
return PdfiumEngineProvider.withPdfium {
|
||||
if (isClosed.get()) {
|
||||
emptyList()
|
||||
} else {
|
||||
pdfPage.getPageLinks().map { ReaderLink(it.uri, it.destPageIdx, it.bounds) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getNativePointer(): Long {
|
||||
|
|
@ -159,29 +215,80 @@ class PdfPageWrapper(val pdfPage: PdfPageKt) : ReaderPage {
|
|||
return 0L
|
||||
}
|
||||
|
||||
override fun close() { pdfPage.close() }
|
||||
override fun close() {
|
||||
if (!isClosed.compareAndSet(false, true)) return
|
||||
PdfiumEngineProvider.withPdfiumBlocking {
|
||||
closePdfiumResource("PdfPageWrapper") { pdfPage.close() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PdfTextPageWrapper(private val textPage: PdfTextPageKt) : ReaderTextPage {
|
||||
override suspend fun textPageCountChars() = textPage.textPageCountChars()
|
||||
override suspend fun textPageGetText(startIndex: Int, count: Int) = textPage.textPageGetText(startIndex, count)
|
||||
override suspend fun textPageGetRectsForRanges(ranges: IntArray) = textPage.textPageGetRectsForRanges(ranges)?.map { ReaderTextRect(it.rect) }
|
||||
override suspend fun textPageGetCharIndexAtPos(x: Double, y: Double, xTolerance: Double, yTolerance: Double) = textPage.textPageGetCharIndexAtPos(x, y, xTolerance, yTolerance)
|
||||
override suspend fun textPageGetCharBox(index: Int) = textPage.textPageGetCharBox(index)
|
||||
override suspend fun textPageGetUnicode(index: Int): Int {
|
||||
return textPage.textPageGetUnicode(index).code
|
||||
private val isClosed = AtomicBoolean(false)
|
||||
|
||||
override suspend fun textPageCountChars() = PdfiumEngineProvider.withPdfium {
|
||||
if (isClosed.get()) 0 else textPage.textPageCountChars()
|
||||
}
|
||||
override suspend fun loadWebLink(): ReaderWebLinks? {
|
||||
val links = textPage.loadWebLink() ?: return null
|
||||
return object : ReaderWebLinks {
|
||||
override suspend fun countWebLinks() = links.countWebLinks()
|
||||
override suspend fun getURL(linkIndex: Int, maxLength: Int) = links.getURL(linkIndex, maxLength)
|
||||
override suspend fun countRects(linkIndex: Int) = links.countRects(linkIndex)
|
||||
override suspend fun getRect(linkIndex: Int, rectIndex: Int) = links.getRect(linkIndex, rectIndex)
|
||||
override fun close() { links.close() }
|
||||
|
||||
override suspend fun textPageGetText(startIndex: Int, count: Int) = PdfiumEngineProvider.withPdfium {
|
||||
if (isClosed.get()) null else textPage.textPageGetText(startIndex, count)
|
||||
}
|
||||
|
||||
override suspend fun textPageGetRectsForRanges(ranges: IntArray) = PdfiumEngineProvider.withPdfium {
|
||||
if (isClosed.get()) null else textPage.textPageGetRectsForRanges(ranges)?.map { ReaderTextRect(it.rect) }
|
||||
}
|
||||
|
||||
override suspend fun textPageGetCharIndexAtPos(x: Double, y: Double, xTolerance: Double, yTolerance: Double) = PdfiumEngineProvider.withPdfium {
|
||||
if (isClosed.get()) -1 else textPage.textPageGetCharIndexAtPos(x, y, xTolerance, yTolerance)
|
||||
}
|
||||
|
||||
override suspend fun textPageGetCharBox(index: Int) = PdfiumEngineProvider.withPdfium {
|
||||
if (isClosed.get()) null else textPage.textPageGetCharBox(index)
|
||||
}
|
||||
|
||||
override suspend fun textPageGetUnicode(index: Int): Int {
|
||||
return PdfiumEngineProvider.withPdfium {
|
||||
if (isClosed.get()) 0 else textPage.textPageGetUnicode(index).code
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun loadWebLink(): ReaderWebLinks? {
|
||||
val links = PdfiumEngineProvider.withPdfium {
|
||||
if (isClosed.get()) null else textPage.loadWebLink()
|
||||
} ?: return null
|
||||
return object : ReaderWebLinks {
|
||||
private val isClosed = AtomicBoolean(false)
|
||||
|
||||
override suspend fun countWebLinks() = PdfiumEngineProvider.withPdfium {
|
||||
if (isClosed.get()) 0 else links.countWebLinks()
|
||||
}
|
||||
|
||||
override suspend fun getURL(linkIndex: Int, maxLength: Int) = PdfiumEngineProvider.withPdfium {
|
||||
if (isClosed.get()) null else links.getURL(linkIndex, maxLength)
|
||||
}
|
||||
|
||||
override suspend fun countRects(linkIndex: Int) = PdfiumEngineProvider.withPdfium {
|
||||
if (isClosed.get()) 0 else links.countRects(linkIndex)
|
||||
}
|
||||
|
||||
override suspend fun getRect(linkIndex: Int, rectIndex: Int) = PdfiumEngineProvider.withPdfium {
|
||||
if (isClosed.get()) RectF() else links.getRect(linkIndex, rectIndex)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
if (!isClosed.compareAndSet(false, true)) return
|
||||
PdfiumEngineProvider.withPdfiumBlocking {
|
||||
closePdfiumResource("PdfWebLinksWrapper") { links.close() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun close() {
|
||||
if (!isClosed.compareAndSet(false, true)) return
|
||||
PdfiumEngineProvider.withPdfiumBlocking {
|
||||
closePdfiumResource("PdfTextPageWrapper") { textPage.close() }
|
||||
}
|
||||
}
|
||||
override fun close() { textPage.close() }
|
||||
}
|
||||
|
||||
// ================= CBZ, CBR, CB7 IMPLEMENTATION =================
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import androidx.paging.PagingConfig
|
|||
import androidx.paging.PagingData
|
||||
import androidx.paging.flatMap
|
||||
import com.aryan.reader.SearchResult
|
||||
import com.aryan.reader.pdf.PdfiumEngineProvider
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
|
|
@ -171,13 +172,15 @@ class PdfTextRepository(context: Context) {
|
|||
var ocrUsed = false
|
||||
|
||||
try {
|
||||
document.openPage(pageIndex)?.use { page ->
|
||||
page.openTextPage().use { textPage ->
|
||||
val count = textPage.textPageCountChars()
|
||||
if (count > 0) {
|
||||
val nativeText = textPage.textPageGetText(0, count)
|
||||
if (!nativeText.isNullOrBlank()) {
|
||||
text = nativeText
|
||||
PdfiumEngineProvider.withPdfium {
|
||||
document.openPage(pageIndex)?.use { page ->
|
||||
page.openTextPage().use { textPage ->
|
||||
val count = textPage.textPageCountChars()
|
||||
if (count > 0) {
|
||||
val nativeText = textPage.textPageGetText(0, count)
|
||||
if (!nativeText.isNullOrBlank()) {
|
||||
text = nativeText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -187,26 +190,35 @@ class PdfTextRepository(context: Context) {
|
|||
}
|
||||
|
||||
if (text.isBlank()) {
|
||||
var bitmap: android.graphics.Bitmap? = null
|
||||
try {
|
||||
document.openPage(pageIndex)?.use { page ->
|
||||
val targetWidth = 1080
|
||||
val ptrWidth = page.getPageWidthPoint()
|
||||
val ptrHeight = page.getPageHeightPoint()
|
||||
PdfiumEngineProvider.withPdfium {
|
||||
document.openPage(pageIndex)?.use { page ->
|
||||
val targetWidth = 1080
|
||||
val ptrWidth = page.getPageWidthPoint()
|
||||
val ptrHeight = page.getPageHeightPoint()
|
||||
|
||||
if (ptrWidth > 0 && ptrHeight > 0) {
|
||||
val aspectRatio = ptrWidth.toFloat() / ptrHeight.toFloat()
|
||||
val targetHeight = (targetWidth / aspectRatio).toInt().coerceAtLeast(1)
|
||||
if (ptrWidth > 0 && ptrHeight > 0) {
|
||||
val aspectRatio = ptrWidth.toFloat() / ptrHeight.toFloat()
|
||||
val targetHeight = (targetWidth / aspectRatio).toInt().coerceAtLeast(1)
|
||||
|
||||
val bitmap = createBitmap(targetWidth, targetHeight)
|
||||
page.renderPageBitmap(bitmap, 0, 0, targetWidth, targetHeight, false)
|
||||
|
||||
val visionText = OcrHelper.extractTextFromBitmap(bitmap, onOcrModelDownloading)
|
||||
bitmap = createBitmap(targetWidth, targetHeight)
|
||||
page.renderPageBitmap(bitmap!!, 0, 0, targetWidth, targetHeight, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
bitmap?.let {
|
||||
try {
|
||||
val visionText = OcrHelper.extractTextFromBitmap(it, onOcrModelDownloading)
|
||||
text = visionText?.text ?: ""
|
||||
bitmap.recycle()
|
||||
ocrUsed = true
|
||||
} finally {
|
||||
it.recycle()
|
||||
bitmap = null
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
bitmap?.recycle()
|
||||
Timber.tag(TAG).e(e, "OCR failed for page $pageIndex")
|
||||
}
|
||||
}
|
||||
|
|
@ -270,11 +282,13 @@ class PdfTextRepository(context: Context) {
|
|||
suspend fun hasNativeText(document: PdfDocumentKt, pageIndex: Int): Boolean {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
document.openPage(pageIndex)?.use { page ->
|
||||
page.openTextPage().use { textPage ->
|
||||
textPage.textPageCountChars() > 0
|
||||
}
|
||||
} ?: false
|
||||
PdfiumEngineProvider.withPdfium {
|
||||
document.openPage(pageIndex)?.use { page ->
|
||||
page.openTextPage().use { textPage ->
|
||||
textPage.textPageCountChars() > 0
|
||||
}
|
||||
} ?: false
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
|
|
@ -585,4 +599,4 @@ class PdfTextRepository(context: Context) {
|
|||
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue