General improvements (#164)

* Optimized PDF tile rendering performance and quality.

* Added support for adjusting TTS voice speed and pitch.

* Updated the TTS settings UI and playback logic to support real-time parameter adjustments.

* Added horizontal scrolling to the PDF viewer bottom toolbar and updated tool arrangement to use fixed spacing.

* Refined cross-page selection and text extraction in `PaginatedReader`

* Updated EPUB reader styling logic to refine typography and layout controls.

* Bump version to 1.0.42(43)
This commit is contained in:
Aryan 2026-04-10 20:23:39 +05:30 committed by GitHub
parent 12d50bb68d
commit 17a0097d4a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 559 additions and 168 deletions

View file

@ -120,9 +120,12 @@ import com.aryan.reader.pdf.data.VirtualPage
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.NonCancellable
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
@ -169,7 +172,7 @@ data class EmbeddedAnnotation(
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)
data class PdfTile(val bitmap: Bitmap, val renderRect: Rect, val tileId: Int, val renderScale: Float = 1f)
enum class LinkSource {
ANNOTATION, TEXT_CONTENT
@ -371,6 +374,7 @@ data class PageSelectionData(
val customHighlightColors: StableHolder<Map<PdfHighlightColor, Color>>
)
@OptIn(FlowPreview::class)
@Suppress("unused")
@Composable
internal fun PdfPageComposable(
@ -1104,12 +1108,15 @@ internal fun PdfPageComposable(
try {
page = withContext(Dispatchers.IO) { pdfDocumentItem.openPage(pdfPageIndex) }
snapshotFlow { visibleScreenRect() }.conflate().collect { currentVisibleRect ->
snapshotFlow { visibleScreenRect() }.conflate().collectLatest { currentVisibleRect ->
delay(150)
val tileCalcStart = System.nanoTime()
if (!isActive) return@collect
if (!isActive) return@collectLatest
if (isScrolling && effectiveScale > 1f) {
return@collect
return@collectLatest
}
val pxTl: Float
@ -1131,20 +1138,16 @@ internal fun PdfPageComposable(
oldTiles.forEach { PdfBitmapPool.recycle(it.bitmap) }
}
}
return@collect
return@collectLatest
}
} else {
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 - 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
}
val visibleBitmapRect = Rect(pxTl.toInt(), pyTl.toInt(), pxBr.toInt(), pyBr.toInt())
@ -1154,13 +1157,11 @@ internal fun PdfPageComposable(
val requiredTileIds = mutableSetOf<Int>()
val cols = (actualBitmapWidthPx + tileSizePx - 1) / tileSizePx
val startCol = (visibleBitmapRect.left / tileSizePx).coerceAtLeast(0)
val endCol =
((visibleBitmapRect.right + tileSizePx - 1) / tileSizePx).coerceAtMost(cols)
val endCol = ((visibleBitmapRect.right + tileSizePx - 1) / tileSizePx).coerceAtMost(cols)
val startRow = (visibleBitmapRect.top / tileSizePx).coerceAtLeast(0)
val endRow =
((visibleBitmapRect.bottom + tileSizePx - 1) / tileSizePx).coerceAtMost(
(actualBitmapHeightPx + tileSizePx - 1) / tileSizePx
)
val endRow = ((visibleBitmapRect.bottom + tileSizePx - 1) / tileSizePx).coerceAtMost(
(actualBitmapHeightPx + tileSizePx - 1) / tileSizePx
)
for (row in startRow until endRow) {
for (col in startCol until endCol) {
@ -1170,6 +1171,9 @@ 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 duration = (System.nanoTime() - tileCalcStart) / 1_000_000f
if (duration > 2f) {
Timber.tag("PdfPerformance").d(
@ -1177,9 +1181,9 @@ internal fun PdfPageComposable(
)
}
if (requiredTileIds != currentTileIds) {
if (requiredTileIds != validCurrentTileIds) {
val tilesToRenderIds = requiredTileIds - currentTileIds
val tilesToRenderIds = requiredTileIds - validCurrentTileIds
val tilesToRecycleIds = currentTileIds - requiredTileIds
if (tilesToRecycleIds.isNotEmpty()) {
@ -1205,15 +1209,12 @@ internal fun PdfPageComposable(
(col + 1) * tileSizePx,
(row + 1) * tileSizePx
)
val tileRenderSize =
(tileSizePx * effectiveScale).toInt().coerceAtLeast(1)
val tileRenderSize = (tileSizePx * effectiveScale).toInt().coerceAtLeast(1)
val tileBitmap = PdfBitmapPool.get(tileRenderSize)
val fullPageRenderWidth =
(actualBitmapWidthPx * effectiveScale).toInt()
val fullPageRenderHeight =
(actualBitmapHeightPx * effectiveScale).toInt()
val fullPageRenderWidth = (actualBitmapWidthPx * effectiveScale).toInt()
val fullPageRenderHeight = (actualBitmapHeightPx * effectiveScale).toInt()
val tileRenderX = (col * tileSizePx * effectiveScale).toInt()
val tileRenderY = (row * tileSizePx * effectiveScale).toInt()
@ -1226,12 +1227,19 @@ internal fun PdfPageComposable(
renderAnnot = true
)
val newTile = PdfTile(tileBitmap, tileRect, tileId)
val newTile = PdfTile(tileBitmap, tileRect, tileId, effectiveScale)
var handedOver = false
try {
withContext(Dispatchers.Main) {
tiles = tiles + newTile
val oldTile = tiles.find { it.tileId == tileId }
tiles = tiles.filter { it.tileId != tileId } + newTile
handedOver = true
oldTile?.let {
coroutineScope.launch(Dispatchers.IO) {
PdfBitmapPool.recycle(it.bitmap)
}
}
}
} finally {
if (!handedOver) {
@ -2746,6 +2754,7 @@ internal fun PdfPageComposable(
} else if (mode == 2 && pointerCount > 1) {
val oldScale = scale
val newScale = (scale * zoomChange).coerceIn(1f, 4f)
Timber.tag("PdfZoomIssue").v("Gesture Scaling: old=$oldScale, new=$newScale, zoomChange=$zoomChange")
val previousCentroid = event.calculateCentroid(useCurrent = false)
if (previousCentroid != Offset.Unspecified) {
@ -3328,8 +3337,12 @@ internal fun PdfPageComposable(
currentPageRotation = 0
val MAX_BASE_DIMEN = 3000
var baseW = scaledWidth
var baseH = scaledHeight
val baseRenderScale = 1.5f
var baseW = (scaledWidth * baseRenderScale).toInt()
var baseH = (scaledHeight * baseRenderScale).toInt()
if (baseW > MAX_BASE_DIMEN || baseH > MAX_BASE_DIMEN) {
val downScale = MAX_BASE_DIMEN.toFloat() / maxOf(baseW, baseH)
baseW = (baseW * downScale).toInt().coerceAtLeast(1)
@ -3394,8 +3407,12 @@ internal fun PdfPageComposable(
}
val MAX_BASE_DIMEN = 3000
var baseW = scaledWidth
var baseH = scaledHeight
val baseRenderScale = 1.5f
var baseW = (scaledWidth * baseRenderScale).toInt()
var baseH = (scaledHeight * baseRenderScale).toInt()
if (baseW > MAX_BASE_DIMEN || baseH > MAX_BASE_DIMEN) {
val downScale = MAX_BASE_DIMEN.toFloat() / maxOf(baseW, baseH)
baseW = (baseW * downScale).toInt().coerceAtLeast(1)

View file

@ -32,6 +32,8 @@ import android.content.Context
import android.content.pm.PackageManager
import androidx.compose.material3.Switch
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Tune
import androidx.compose.foundation.rememberScrollState
import android.graphics.Bitmap
import android.graphics.RectF
import android.net.Uri
@ -78,6 +80,7 @@ import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.gestures.waitForUpOrCancellation
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsDraggedAsState
import androidx.compose.foundation.layout.Arrangement
@ -284,6 +287,7 @@ import com.aryan.reader.countWords
import com.aryan.reader.epubreader.AutoScrollControls
import com.aryan.reader.epubreader.DictionarySettingsDialog
import com.aryan.reader.epubreader.ExternalDictionaryHelper
import com.aryan.reader.epubreader.TtsControlsSheet
import com.aryan.reader.fetchAiDefinition
import com.aryan.reader.loadCustomThemes
import com.aryan.reader.paginatedreader.TtsChunk
@ -1322,6 +1326,7 @@ fun PdfViewerScreen(
var isStylusOnlyMode by remember { mutableStateOf(loadStylusOnlyMode(context)) }
var currentTtsMode by remember { mutableStateOf(loadTtsMode(context)) }
var showTtsSettingsSheet by remember { mutableStateOf(false) }
var showTtsControlsSheet by remember { mutableStateOf(false) }
var isKeepScreenOn by remember { mutableStateOf(loadKeepScreenOn(context)) }
DisposableEffect(isKeepScreenOn) {
@ -6322,12 +6327,15 @@ fun PdfViewerScreen(
color = MaterialTheme.colorScheme.surface,
tonalElevation = 4.dp
) {
val bottomBarScrollState = rememberScrollState()
Row(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 8.dp),
.padding(horizontal = 8.dp)
.horizontalScroll(bottomBarScrollState),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceAround
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
// Slider Navigation Trigger
if (!hiddenTools.contains(PdfReaderTool.SLIDER.name)) {
@ -6498,58 +6506,73 @@ fun PdfViewerScreen(
// TTS
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 = {
if (isTtsSessionActive) {
Timber.d("TTS button clicked: Stopping TTS")
ttsController.stop()
} else {
startTtsWithPermissionCheck(null, null)
Box {
Row(verticalAlignment = Alignment.CenterVertically) {
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 = {
if (isTtsSessionActive) {
Timber.d("TTS button clicked: Stopping TTS")
ttsController.stop()
} else {
startTtsWithPermissionCheck(null, null)
}
}) {
Icon(
painter = if (isTtsSessionActive) painterResource(id = R.drawable.close)
else painterResource(
id = R.drawable.text_to_speech
),
contentDescription = if (isTtsSessionActive) "Stop TTS" else "Start TTS"
)
}
}) {
Icon(
painter = if (isTtsSessionActive) painterResource(id = R.drawable.close)
else painterResource(
id = R.drawable.text_to_speech
),
contentDescription = if (isTtsSessionActive) "Stop TTS" else "Start TTS"
)
}
}
// TTS Pause/Resume Button
if (isTtsSessionActive) {
TooltipIconButton(
text = if (ttsState.isPlaying)
stringResource(R.string.tooltip_tts_pause)
else
stringResource(R.string.tooltip_tts_resume),
description = if (ttsState.isPlaying)
stringResource(R.string.tooltip_tts_pause_desc)
else
stringResource(R.string.tooltip_tts_resume_desc),
onClick = {
if (ttsState.isPlaying) {
ttsController.pause()
} else {
ttsController.resume()
if (isTtsSessionActive) {
TooltipIconButton(
text = if (ttsState.isPlaying)
stringResource(R.string.tooltip_tts_pause)
else
stringResource(R.string.tooltip_tts_resume),
description = if (ttsState.isPlaying)
stringResource(R.string.tooltip_tts_pause_desc)
else
stringResource(R.string.tooltip_tts_resume_desc),
onClick = {
if (ttsState.isPlaying) {
ttsController.pause()
} else {
ttsController.resume()
}
}, enabled = !ttsState.isLoading
) {
Icon(
painter = painterResource(
id = if (ttsState.isPlaying) R.drawable.pause
else R.drawable.play
), contentDescription = if (ttsState.isPlaying) "Pause TTS"
else "Resume TTS"
)
}
// Tune button for BASE mode
if (currentTtsMode == TtsPlaybackManager.TtsMode.BASE) {
TooltipIconButton(
text = "Voice Adjustments",
description = "Adjust voice speed and pitch",
onClick = { showTtsControlsSheet = true }
) {
Icon(
imageVector = Icons.Default.Tune,
contentDescription = "Voice Adjustments"
)
}
}
}
}, enabled = !ttsState.isLoading
) {
Icon(
painter = painterResource(
id = if (ttsState.isPlaying) R.drawable.pause
else R.drawable.play
), contentDescription = if (ttsState.isPlaying) "Pause TTS"
else "Resume TTS"
)
}
}
} else {
Spacer(Modifier.size(48.dp))
}
// Error Message Area
@ -7458,6 +7481,14 @@ fun PdfViewerScreen(
)
}
if (showTtsControlsSheet) {
TtsControlsSheet(
onDismiss = { showTtsControlsSheet = false },
onOpenDeviceVoiceSettings = { showDeviceVoiceSettingsSheet = true },
ttsController = ttsController
)
}
if (showDictionarySettingsSheet) {
DictionarySettingsDialog(
isVisible = true,