Epub improvements (#162)

* Added a "Remove Edge Padding" option in "Visual Options"

* Added support for paragraph gap settings in the EPUB vertical reader.

* Added support for adjustable paragraph gaps in the paginated reader.

* fix(epub-pagination): ensure custom fonts override embedded epub fonts

* feat(epub): allow overlapping highlights and resolve gesture selection conflicts
This commit is contained in:
Aryan 2026-04-10 17:43:01 +05:30 committed by GitHub
parent 291504fd90
commit 49e08cc9f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 315 additions and 264 deletions

View file

@ -491,6 +491,7 @@ private fun WrappingContentLayout(
@OptIn(ExperimentalFoundationApi::class, ExperimentalSerializationApi::class, FlowPreview::class)
@Composable
fun PaginatedReaderScreen(
modifier: Modifier = Modifier,
book: EpubBook,
isDarkTheme: Boolean,
effectiveBg: Color,
@ -500,11 +501,12 @@ fun PaginatedReaderScreen(
searchQuery: String,
fontSizeMultiplier: Float,
lineHeightMultiplier: Float,
paragraphGapMultiplier: Float,
fontFamily: FontFamily,
textAlign: ReaderTextAlign,
ttsHighlightInfo: TtsHighlightInfo?,
initialChapterIndexInBook: Int?,
modifier: Modifier = Modifier,
removeEdgePadding: Boolean = false,
onPaginatorReady: (IPaginator) -> Unit,
onTap: (Offset?) -> Unit,
isProUser: Boolean,
@ -553,6 +555,7 @@ fun PaginatedReaderScreen(
var debouncedFontSizeMult by remember { mutableFloatStateOf(fontSizeMultiplier) }
var debouncedLineHeightMult by remember { mutableFloatStateOf(lineHeightMultiplier) }
var debouncedParagraphGapMult by remember { mutableFloatStateOf(paragraphGapMultiplier) }
var debouncedFontFamily by remember { mutableStateOf(fontFamily) }
var debouncedTextAlign by remember { mutableStateOf(textAlign) }
@ -611,8 +614,8 @@ fun PaginatedReaderScreen(
)
}
LaunchedEffect(fontSizeMultiplier, lineHeightMultiplier, fontFamily, textAlign) {
if (fontSizeMultiplier != debouncedFontSizeMult || lineHeightMultiplier != debouncedLineHeightMult || fontFamily != debouncedFontFamily || textAlign != debouncedTextAlign) {
LaunchedEffect(fontSizeMultiplier, lineHeightMultiplier, paragraphGapMultiplier, fontFamily, textAlign) {
if (fontSizeMultiplier != debouncedFontSizeMult || lineHeightMultiplier != debouncedLineHeightMult || paragraphGapMultiplier != debouncedParagraphGapMult || fontFamily != debouncedFontFamily || textAlign != debouncedTextAlign) {
Timber.d("Formatting changed. Waiting for debounce.")
delay(400L)
@ -627,6 +630,7 @@ fun PaginatedReaderScreen(
debouncedFontSizeMult = fontSizeMultiplier
debouncedLineHeightMult = lineHeightMultiplier
debouncedParagraphGapMult = paragraphGapMultiplier
debouncedFontFamily = fontFamily
debouncedTextAlign = textAlign
Timber.d("Debounce complete. Applying new format settings.")
@ -642,7 +646,7 @@ fun PaginatedReaderScreen(
}
val density = LocalDensity.current
val horizontalPadding = 16.dp
val horizontalPadding = if (removeEdgePadding) 0.dp else 16.dp
val verticalPadding = 16.dp
val textConstraints =
@ -673,8 +677,8 @@ fun PaginatedReaderScreen(
remember(initialChapterIndexInBook, anchorLocatorForReconfig) {
anchorLocatorForReconfig?.chapterIndex ?: initialChapterIndexInBook ?: 0
}
val paginator = remember(book, textConstraints, isDarkTheme, textStyle, userTextAlign, effectiveBg, effectiveText) {
val userAgentStylesheet = UserAgentStylesheet.default
val paginator = remember(book, textConstraints, isDarkTheme, textStyle, userTextAlign, effectiveBg, effectiveText, debouncedParagraphGapMult) {
val userAgentStylesheet = UserAgentStylesheet.default
var allRules = OptimizedCssRules()
val allFontFaces = mutableListOf<FontFaceInfo>()
@ -739,7 +743,8 @@ fun PaginatedReaderScreen(
allFontFaces = allFontFaces,
context = context.applicationContext,
mathMLRenderer = mathMLRenderer,
userTextAlign = userTextAlign
userTextAlign = userTextAlign,
paragraphGapMultiplier = debouncedParagraphGapMult
)
}
@ -1171,7 +1176,6 @@ private fun TextWithEmphasis(
is HeaderBlock -> block.startCharOffsetInSource
is QuoteBlock -> block.startCharOffsetInSource
is ListItemBlock -> block.startCharOffsetInSource
else -> 0
}
val isStart = block.blockIndex == activeSelection.startBlockIndex && currentBlockAbs == activeSelection.startBlockCharOffset
@ -1276,76 +1280,6 @@ private fun TextWithEmphasis(
}
.then(customDrawer)
.pointerInput(userHighlights, text) {
awaitEachGesture {
val down = awaitFirstDown(
pass = PointerEventPass.Initial, requireUnconsumed = false
)
val layout = textLayoutResult
if (layout != null) {
val hit = getHighlightAt(down.position, layout)
if (hit != null) {
down.consume()
val startPosition = down.position
var dragDistance = 0f
var isFinished = false
val longPressJob = scope.launch {
delay(500)
pressedHighlightCfi = hit.first.cfi
}
try {
while (true) {
val event = awaitPointerEvent(
pass = PointerEventPass.Initial
)
val change = event.changes.firstOrNull {
it.id == down.id
}
if (change == null) {
longPressJob.cancel()
pressedHighlightCfi = null
break
}
change.consume()
if (change.pressed) {
dragDistance = (change.position - startPosition).getDistance()
if (dragDistance >= viewConfiguration.touchSlop) {
longPressJob.cancel()
pressedHighlightCfi = null
}
} else {
isFinished = true
longPressJob.cancel()
pressedHighlightCfi = null
break
}
}
} catch (_: Exception) {
longPressJob.cancel()
pressedHighlightCfi = null
}
if (isFinished && dragDistance < viewConfiguration.touchSlop) {
val (highlight, localRect) = hit
val globalRect = layoutCoordinates?.let { coords ->
if (coords.isAttached) {
val topLeft = coords.localToWindow(
localRect.topLeft
)
val bottomRight = coords.localToWindow(
localRect.bottomRight
)
Rect(topLeft, bottomRight)
} else null
} ?: localRect
onHighlightClick(highlight, globalRect)
}
}
}
}
}
.pointerInput(text) {
detectTapGestures(
onLongPress = { offset ->
textLayoutResult?.let { layout ->
@ -1355,7 +1289,6 @@ private fun TextWithEmphasis(
var start = wordBoundary.start
var end = wordBoundary.end
// Trim trailing/leading punctuations for a cleaner word selection
val textStr = text.text
while (start < end && start < textStr.length && !textStr[start].isLetterOrDigit()) start++
while (end > start && end <= textStr.length && !textStr[end - 1].isLetterOrDigit()) end--
@ -1377,7 +1310,6 @@ private fun TextWithEmphasis(
is HeaderBlock -> block.startCharOffsetInSource
is QuoteBlock -> block.startCharOffsetInSource
is ListItemBlock -> block.startCharOffsetInSource
else -> 0
}
onSelectionChange(
@ -1402,14 +1334,27 @@ private fun TextWithEmphasis(
},
onTap = { offset ->
textLayoutResult?.let { layout ->
val hit = getHighlightAt(offset, layout)
if (hit != null) {
val (highlight, localRect) = hit
val globalRect = layoutCoordinates?.let { coords ->
if (coords.isAttached) {
val topLeft = coords.localToWindow(localRect.topLeft)
val bottomRight = coords.localToWindow(localRect.bottomRight)
Rect(topLeft, bottomRight)
} else null
} ?: localRect
onHighlightClick(highlight, globalRect)
return@detectTapGestures
}
val charOffset = layout.getOffsetForPosition(offset)
val urlAnnotation = text.getStringAnnotations(
"URL", charOffset, charOffset
).firstOrNull()
val urlAnnotation = text.getStringAnnotations("URL", charOffset, charOffset).firstOrNull()
if (urlAnnotation != null) onLinkClick(urlAnnotation.item)
else onGeneralTap(offset)
}
})
}
)
}, onTextLayout = {
textLayoutResult = it
if (layoutCoordinates != null && block.cfi != null) {
@ -1713,7 +1658,6 @@ internal fun PaginatedReaderContent(
is HeaderBlock -> firstTextBlock.startCharOffsetInSource
is QuoteBlock -> firstTextBlock.startCharOffsetInSource
is ListItemBlock -> firstTextBlock.startCharOffsetInSource
else -> 0
}
val newTextPerBlock = (previousSel?.textPerBlock ?: emptyMap()).toMutableMap()
@ -2853,7 +2797,6 @@ internal fun PaginatedReaderContent(
is HeaderBlock -> block.startCharOffsetInSource
is QuoteBlock -> block.startCharOffsetInSource
is ListItemBlock -> block.startCharOffsetInSource
else -> 0
}
val isStartBlockPart = block.blockIndex == sel.startBlockIndex && currentBlockAbs == sel.startBlockCharOffset
val isEndBlockPart = block.blockIndex == sel.endBlockIndex && currentBlockAbs == sel.endBlockCharOffset
@ -3008,7 +2951,6 @@ internal fun PaginatedReaderContent(
is HeaderBlock -> block.startCharOffsetInSource
is QuoteBlock -> block.startCharOffsetInSource
is ListItemBlock -> block.startCharOffsetInSource
else -> 0
}
var newStartBlockAbs = if (isStartHandle) currentBlockAbs else sel.startBlockCharOffset
var newEndBlockAbs = if (!isStartHandle) currentBlockAbs else sel.endBlockCharOffset
@ -3044,7 +2986,6 @@ internal fun PaginatedReaderContent(
is HeaderBlock -> (b.third as HeaderBlock).startCharOffsetInSource
is QuoteBlock -> (b.third as QuoteBlock).startCharOffsetInSource
is ListItemBlock -> (b.third as ListItemBlock).startCharOffsetInSource
else -> 0
}
}))
@ -3061,7 +3002,6 @@ internal fun PaginatedReaderContent(
is HeaderBlock -> b.third.startCharOffsetInSource
is QuoteBlock -> b.third.startCharOffsetInSource
is ListItemBlock -> b.third.startCharOffsetInSource
else -> 0
}
val isStartBlockPart = b.third.blockIndex == newStartIdx && bAbs == newStartBlockAbs
val isEndBlockPart = b.third.blockIndex == newEndIdx && bAbs == newEndBlockAbs
@ -3092,7 +3032,6 @@ internal fun PaginatedReaderContent(
is HeaderBlock -> it.third.startCharOffsetInSource
is QuoteBlock -> it.third.startCharOffsetInSource
is ListItemBlock -> it.third.startCharOffsetInSource
else -> 0
}
abs == newStartBlockAbs
}
@ -3187,7 +3126,6 @@ internal fun PaginatedReaderContent(
is HeaderBlock -> block.startCharOffsetInSource
is QuoteBlock -> block.startCharOffsetInSource
is ListItemBlock -> block.startCharOffsetInSource
else -> 0
}
blockAbs == targetBlockAbs
}