V1.0.46 oss (#253)
* add a UI trigger for demo annotations * Implement internal link navigation * Simplify bottom padding logic in `EpubReaderScreen` for vertical scroll mode * Add support for Calibre metadata * Implement advanced library management with Room-backed collections, tags, and smart rules. * Display book series information in book details and remove tags from home screen * Implement zoom reset functionality in PDF viewer * Add a "Pages" thumbnail view to the PDF navigation drawer * Implement jump-back navigation in PDF viewer * MainViewModel: disable FolderSyncWorker, add log export, and enhance PDF position logging * Replace Gson with Kotlin Serialization in SmartCollectionEngine * Implement ONNX-based speech bubble detection * Implement speech bubble detection in PDF viewer * Implement "Smart Comic Zoom" feature for PDF manga/comic reading * Implement reader session persistence and restoration in `MainViewModel` * Add tap-to-turn page feature to PDF viewer * Improve book cache management and recovery * Refactor top overlay padding logic in `PdfViewerScreen` * Implement stylus eraser support in PDF reader * Refactor ReaderTextFormatPanel to use ModalBottomSheet and add new formatting controls * Introduce a customizable horizontal margin setting for the EPUB reader * Refine folder sync and metadata handling for better conflict resolution and stability. * Introduce user-adjustable image scaling for the EPUB reader * Use maxWidthPx as default width fallback for blocks * Improve cross-page text selection and header styling in the paginated reader * Refactor speech bubble detection and UI to support segmentation masks and interactive scaling * Improve speech bubble detection masking and rendering quality * Update ONNX speech bubble detector to use `.ort` model and optimize inference * Improve pagination accuracy * Implement hierarchical folder navigation for the library. * Refactor library item layout for improved space efficiency * Add support for browsing and downloading Google Fonts * Persist library landing state and add shelf search functionality * Fix base tts sample. * Move SpeechBubbleDetector to main source set and update ONNX dependency * Implement a "locate" feature and improve synchronization for TTS (Text-to-Speech) playback across EPUB and PDF readers. * Refine TTS synchronization and voice management in the EPUB reader * Adjust OCR checks for OSS flavor and optimize external dictionary intent flags * Add "Locate" button to PDF drawer's pages tab and move the page number to bottom right of thumbnail * fix various crashes * Refactor SpeechBubbleDetector into product flavors * Implement speech bubble detection caching and background prefetching * Implement on-demand download for Bubble Zoom ML model * Add smooth animation for PDF speech bubble expansion * fixes #252 * hide Google Fonts option, in FontsScreen, for offline variant * Bump version to 1.0.46(46)
This commit is contained in:
parent
579b6f25bf
commit
d16bdd70d4
67 changed files with 9724 additions and 1890 deletions
|
|
@ -119,7 +119,8 @@ class BookPaginator(
|
|||
private val context: Context,
|
||||
private val mathMLRenderer: MathMLRenderer,
|
||||
private val userTextAlign: TextAlign?,
|
||||
private val paragraphGapMultiplier: Float
|
||||
private val paragraphGapMultiplier: Float,
|
||||
private val imageSizeMultiplier: Float
|
||||
) : IPaginator {
|
||||
override var totalPageCount by mutableIntStateOf(0)
|
||||
private set
|
||||
|
|
@ -267,7 +268,16 @@ class BookPaginator(
|
|||
}
|
||||
|
||||
private fun generateConfigurationHash(): Int {
|
||||
val configString = "w:${constraints.maxWidth}-h:${constraints.maxHeight}-fs:${textStyle.fontSize.value}-ta:$userTextAlign-pg:$paragraphGapMultiplier"
|
||||
val configString = buildString {
|
||||
append("w:${constraints.maxWidth}")
|
||||
append("-h:${constraints.maxHeight}")
|
||||
append("-fs:${textStyle.fontSize.value}")
|
||||
append("-lh:${textStyle.lineHeight.value}")
|
||||
append("-ff:${textStyle.fontFamily}")
|
||||
append("-ta:$userTextAlign")
|
||||
append("-pg:$paragraphGapMultiplier")
|
||||
append("-img:$imageSizeMultiplier")
|
||||
}
|
||||
val hash = configString.hashCode()
|
||||
return hash
|
||||
}
|
||||
|
|
@ -722,7 +732,8 @@ class BookPaginator(
|
|||
textMeasurer = textMeasurer,
|
||||
constraints = constraints,
|
||||
textStyle = textStyle,
|
||||
density = density
|
||||
density = density,
|
||||
imageSizeMultiplier = imageSizeMultiplier
|
||||
)
|
||||
Timber.d("paginateChapter: Calling PaginatorLogic for chapter $chapterIndex.")
|
||||
val pages = paginate(
|
||||
|
|
@ -971,31 +982,7 @@ class BookPaginator(
|
|||
return@launch
|
||||
}
|
||||
|
||||
val chapterPages = pageCache[targetChapterIndex] ?: paginateChapter(targetChapterIndex)
|
||||
val chapterStartPage = calculateAccurateStartIndex(targetChapterIndex)
|
||||
|
||||
if (chapterPages == null) {
|
||||
Timber.e("Href Navigation failed: Could not paginate target chapter $targetChapterIndex.")
|
||||
return@launch
|
||||
}
|
||||
|
||||
var targetPageInChapter = 0
|
||||
if (anchor != null) {
|
||||
Timber.d("Searching for anchor '$anchor' in chapter $targetChapterIndex.")
|
||||
pageLoop@ for ((pageIndex, page) in chapterPages.withIndex()) {
|
||||
for (block in page.content) {
|
||||
if (block.elementId == anchor) {
|
||||
targetPageInChapter = pageIndex
|
||||
Timber.i("Found anchor '$anchor' on page $pageIndex in chapter $targetChapterIndex")
|
||||
break@pageLoop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val finalPageIndex = chapterStartPage + targetPageInChapter
|
||||
Timber.i("Href navigation complete. Final page index: $finalPageIndex")
|
||||
withContext(Dispatchers.Main) { onNavigationComplete(finalPageIndex) }
|
||||
findPageForAnchor(targetChapterIndex, anchor, onNavigationComplete)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1304,4 +1291,4 @@ class BookPaginator(
|
|||
return null to null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -144,7 +144,8 @@ class PaginatedReaderViewModel : ViewModel() {
|
|||
context = context.applicationContext,
|
||||
mathMLRenderer = mathMLRenderer,
|
||||
userTextAlign = null,
|
||||
paragraphGapMultiplier = paragraphGapMultiplier
|
||||
paragraphGapMultiplier = paragraphGapMultiplier,
|
||||
imageSizeMultiplier = 1.0f
|
||||
)
|
||||
paginator = newPaginator
|
||||
|
||||
|
|
@ -174,4 +175,4 @@ class PaginatedReaderViewModel : ViewModel() {
|
|||
fun onLinkClick(currentChapterPath: String, href: String, onNavigationComplete: (Int) -> Unit) {
|
||||
paginator?.navigateToHref(currentChapterPath, href, onNavigationComplete)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,8 @@ class SuspendingAndroidBlockMeasurementProvider(
|
|||
private val textMeasurer: TextMeasurer,
|
||||
private val constraints: Constraints,
|
||||
private val textStyle: TextStyle,
|
||||
private val density: Density
|
||||
private val density: Density,
|
||||
private val imageSizeMultiplier: Float
|
||||
) : BlockMeasurementProvider {
|
||||
|
||||
override suspend fun measure(block: ContentBlock): Int {
|
||||
|
|
@ -60,7 +61,8 @@ class SuspendingAndroidBlockMeasurementProvider(
|
|||
constraints = constraints,
|
||||
defaultStyle = textStyle,
|
||||
headerStyle = textStyle.copy(fontWeight = FontWeight.Bold),
|
||||
density = density
|
||||
density = density,
|
||||
imageSizeMultiplier = imageSizeMultiplier
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -79,29 +81,12 @@ class SuspendingAndroidBlockMeasurementProvider(
|
|||
|
||||
val imageBlock = block.floatedImage
|
||||
val (imageWidthPx, imageHeightPx) = run {
|
||||
val imageStyle = imageBlock.style
|
||||
val intrinsicWidth = imageBlock.intrinsicWidth
|
||||
val intrinsicHeight = imageBlock.intrinsicHeight
|
||||
|
||||
if (intrinsicWidth == null || intrinsicHeight == null || intrinsicWidth <= 0f) {
|
||||
0f to 0f
|
||||
} else {
|
||||
val aspectRatio = intrinsicHeight / intrinsicWidth
|
||||
val renderWidth = with(density) {
|
||||
var w = intrinsicWidth
|
||||
|
||||
if (imageStyle.width != Dp.Unspecified) {
|
||||
w = imageStyle.width.toPx()
|
||||
}
|
||||
|
||||
if (imageStyle.maxWidth != Dp.Unspecified) {
|
||||
w = w.coerceAtMost(imageStyle.maxWidth.toPx())
|
||||
}
|
||||
|
||||
w.coerceAtMost(constraints.maxWidth.toFloat())
|
||||
}
|
||||
renderWidth to (renderWidth * aspectRatio)
|
||||
}
|
||||
measureScaledImageSizePx(
|
||||
block = imageBlock,
|
||||
density = density,
|
||||
maxWidthPx = constraints.maxWidth.toFloat(),
|
||||
imageSizeMultiplier = imageSizeMultiplier
|
||||
)
|
||||
}
|
||||
|
||||
if (imageWidthPx <= 0 || imageHeightPx <= 0) {
|
||||
|
|
@ -668,62 +653,25 @@ private suspend fun measureBlockHeight(
|
|||
constraints: Constraints,
|
||||
defaultStyle: TextStyle,
|
||||
headerStyle: TextStyle,
|
||||
density: Density
|
||||
density: Density,
|
||||
imageSizeMultiplier: Float = 1.0f
|
||||
): Int {
|
||||
var verticalPaddingPx = 0f
|
||||
var horizontalPaddingPx = 0f
|
||||
var verticalBorderPx = 0f
|
||||
var horizontalBorderPx = 0f
|
||||
|
||||
with(density) {
|
||||
verticalPaddingPx = block.style.padding.top.toPx() + block.style.padding.bottom.toPx()
|
||||
horizontalPaddingPx = block.style.padding.left.toPx() + block.style.padding.right.toPx()
|
||||
|
||||
verticalBorderPx = (block.style.borderTop?.width?.toPx() ?: 0f) + (block.style.borderBottom?.width?.toPx() ?: 0f)
|
||||
horizontalBorderPx = (block.style.borderLeft?.width?.toPx() ?: 0f) + (block.style.borderRight?.width?.toPx() ?: 0f)
|
||||
}
|
||||
|
||||
val isBorderBox = block.style.boxSizing == "border-box"
|
||||
val specifiedWidthDp = block.style.width
|
||||
val specifiedMaxWidthDp = block.style.maxWidth
|
||||
|
||||
val blockOuterWidthPx = with(density) {
|
||||
var effectiveWidthPx = constraints.maxWidth.toFloat()
|
||||
if (specifiedWidthDp != Dp.Unspecified) {
|
||||
effectiveWidthPx = specifiedWidthDp.toPx()
|
||||
}
|
||||
if (specifiedMaxWidthDp != Dp.Unspecified) {
|
||||
val maxWidthPx = specifiedMaxWidthDp.toPx()
|
||||
if (effectiveWidthPx > maxWidthPx) {
|
||||
effectiveWidthPx = maxWidthPx
|
||||
}
|
||||
}
|
||||
effectiveWidthPx.coerceAtMost(constraints.maxWidth.toFloat())
|
||||
}
|
||||
|
||||
val contentMaxWidth = if (specifiedWidthDp == Dp.Unspecified) {
|
||||
(blockOuterWidthPx - horizontalPaddingPx - horizontalBorderPx)
|
||||
} else if (isBorderBox) {
|
||||
(blockOuterWidthPx - horizontalPaddingPx - horizontalBorderPx)
|
||||
} else {
|
||||
blockOuterWidthPx
|
||||
}
|
||||
|
||||
val adjustedConstraints = constraints.copy(
|
||||
maxWidth = contentMaxWidth.roundToInt().coerceAtLeast(0),
|
||||
maxHeight = Constraints.Infinity
|
||||
)
|
||||
val boxMetrics = computeBlockBoxMetrics(block, constraints, density)
|
||||
val verticalPaddingPx = boxMetrics.verticalPaddingPx
|
||||
val verticalBorderPx = boxMetrics.verticalBorderPx
|
||||
val adjustedConstraints = boxMetrics.contentConstraints
|
||||
|
||||
val contentHeight = when (block) {
|
||||
is ParagraphBlock -> {
|
||||
val paragraphStyle = defaultStyle.copy(textAlign = block.textAlign ?: defaultStyle.textAlign)
|
||||
val height = withContext(Dispatchers.Main) {
|
||||
textMeasurer.measure(
|
||||
text = block.content,
|
||||
style = defaultStyle.copy(textAlign = block.textAlign ?: defaultStyle.textAlign),
|
||||
style = paragraphStyle,
|
||||
constraints = adjustedConstraints
|
||||
).size.height
|
||||
}
|
||||
height
|
||||
height + centeredTextSafetyPaddingPx(paragraphStyle, density)
|
||||
}
|
||||
is HeaderBlock -> {
|
||||
val style = headerStyle.copy(
|
||||
|
|
@ -736,27 +684,15 @@ private suspend fun measureBlockHeight(
|
|||
constraints = adjustedConstraints
|
||||
).size.height
|
||||
}
|
||||
height
|
||||
height + centeredTextSafetyPaddingPx(style, density)
|
||||
}
|
||||
is ImageBlock -> {
|
||||
val imageIntrinsicWidth = block.intrinsicWidth
|
||||
val imageIntrinsicHeight = block.intrinsicHeight
|
||||
|
||||
val styledHeightPx = if (block.style.height.isSpecified) with(density) { block.style.height.toPx() } else null
|
||||
val styledWidthPx = if (block.style.width.isSpecified) with(density) { block.style.width.toPx() } else null
|
||||
|
||||
val measuredHeight = when {
|
||||
styledHeightPx != null && styledHeightPx > 0f -> styledHeightPx
|
||||
styledWidthPx != null && styledWidthPx > 0f && imageIntrinsicWidth != null && imageIntrinsicHeight != null && imageIntrinsicWidth > 0 -> {
|
||||
val aspectRatio = imageIntrinsicHeight / imageIntrinsicWidth
|
||||
styledWidthPx * aspectRatio
|
||||
}
|
||||
imageIntrinsicWidth != null && imageIntrinsicHeight != null && imageIntrinsicWidth > 0 -> {
|
||||
val aspectRatio = imageIntrinsicHeight / imageIntrinsicWidth
|
||||
contentMaxWidth * aspectRatio
|
||||
}
|
||||
else -> with(density) { 250.dp.toPx() }
|
||||
}
|
||||
val measuredHeight = measureScaledImageHeightPx(
|
||||
block = block,
|
||||
density = density,
|
||||
contentMaxWidth = adjustedConstraints.maxWidth.toFloat(),
|
||||
imageSizeMultiplier = imageSizeMultiplier
|
||||
) ?: with(density) { 250.dp.toPx() }
|
||||
|
||||
val finalHeight = measuredHeight.coerceAtMost(constraints.maxHeight.toFloat()).roundToInt()
|
||||
Timber.tag("IMAGE_DIAG").d("Measured Image [#${block.blockIndex}]: $finalHeight px (Capped at ${constraints.maxHeight})")
|
||||
|
|
@ -767,14 +703,15 @@ private suspend fun measureBlockHeight(
|
|||
height
|
||||
}
|
||||
is QuoteBlock -> {
|
||||
val quoteStyle = defaultStyle.copy(textAlign = block.textAlign ?: defaultStyle.textAlign)
|
||||
val height = withContext(Dispatchers.Main) {
|
||||
textMeasurer.measure(
|
||||
text = block.content,
|
||||
style = defaultStyle.copy(textAlign = block.textAlign ?: defaultStyle.textAlign),
|
||||
style = quoteStyle,
|
||||
constraints = adjustedConstraints
|
||||
).size.height
|
||||
}
|
||||
height
|
||||
height + centeredTextSafetyPaddingPx(quoteStyle, density)
|
||||
}
|
||||
is ListItemBlock -> {
|
||||
val markerWidthPx = with(density) { 32.dp.toPx() }.toInt()
|
||||
|
|
@ -811,7 +748,7 @@ private suspend fun measureBlockHeight(
|
|||
|
||||
val cellConstraints = adjustedConstraints.copy(maxWidth = cellMaxWidth.coerceAtLeast(0))
|
||||
|
||||
val cellContentHeight = calculateContentHeightWithMargins(cell.content, textMeasurer, cellConstraints, defaultStyle, headerStyle, density)
|
||||
val cellContentHeight = calculateContentHeightWithMargins(cell.content, textMeasurer, cellConstraints, defaultStyle, headerStyle, density, imageSizeMultiplier)
|
||||
|
||||
var cellDecorationHeight = 0f
|
||||
with(density) {
|
||||
|
|
@ -829,35 +766,18 @@ private suspend fun measureBlockHeight(
|
|||
val imageBlock = block.floatedImage
|
||||
|
||||
val (imageWidthPx, imageHeightPx) = run {
|
||||
val imageStyle = imageBlock.style
|
||||
val intrinsicWidth = imageBlock.intrinsicWidth
|
||||
val intrinsicHeight = imageBlock.intrinsicHeight
|
||||
|
||||
if (intrinsicWidth == null || intrinsicHeight == null || intrinsicWidth <= 0f) {
|
||||
0f to 0f
|
||||
} else {
|
||||
val aspectRatio = intrinsicHeight / intrinsicWidth
|
||||
val renderWidth = with(density) {
|
||||
var w = intrinsicWidth
|
||||
|
||||
if (imageStyle.width != Dp.Unspecified) {
|
||||
w = imageStyle.width.toPx()
|
||||
}
|
||||
|
||||
if (imageStyle.maxWidth != Dp.Unspecified) {
|
||||
w = w.coerceAtMost(imageStyle.maxWidth.toPx())
|
||||
}
|
||||
|
||||
w.coerceAtMost(adjustedConstraints.maxWidth.toFloat())
|
||||
}
|
||||
renderWidth to (renderWidth * aspectRatio)
|
||||
}
|
||||
measureScaledImageSizePx(
|
||||
block = imageBlock,
|
||||
density = density,
|
||||
maxWidthPx = adjustedConstraints.maxWidth.toFloat(),
|
||||
imageSizeMultiplier = imageSizeMultiplier
|
||||
)
|
||||
}
|
||||
|
||||
// If image has no size, it can't float. Just measure the paragraphs.
|
||||
if (imageWidthPx <= 0 || imageHeightPx <= 0) {
|
||||
val height = block.paragraphsToWrap.sumOf { p ->
|
||||
measureBlockHeight(p, textMeasurer, adjustedConstraints, defaultStyle, headerStyle, density)
|
||||
measureBlockHeight(p, textMeasurer, adjustedConstraints, defaultStyle, headerStyle, density, imageSizeMultiplier)
|
||||
}
|
||||
return height
|
||||
}
|
||||
|
|
@ -949,10 +869,10 @@ private suspend fun measureBlockHeight(
|
|||
val isRow = block.style.flexDirection == "row"
|
||||
val height = if (isRow) {
|
||||
block.children.maxOfOrNull { child ->
|
||||
measureBlockHeight(child, textMeasurer, adjustedConstraints, defaultStyle, headerStyle, density)
|
||||
measureBlockHeight(child, textMeasurer, adjustedConstraints, defaultStyle, headerStyle, density, imageSizeMultiplier)
|
||||
} ?: 0
|
||||
} else {
|
||||
calculateContentHeightWithMargins(block.children, textMeasurer, adjustedConstraints, defaultStyle, headerStyle, density)
|
||||
calculateContentHeightWithMargins(block.children, textMeasurer, adjustedConstraints, defaultStyle, headerStyle, density, imageSizeMultiplier)
|
||||
}
|
||||
height
|
||||
}
|
||||
|
|
@ -980,7 +900,7 @@ private suspend fun measureBlockHeight(
|
|||
}
|
||||
}
|
||||
val specifiedHeightDp = block.style.height
|
||||
val finalHeight = if (isBorderBox && specifiedHeightDp != Dp.Unspecified) {
|
||||
val finalHeight = if (block.style.boxSizing == "border-box" && specifiedHeightDp != Dp.Unspecified) {
|
||||
with(density) { specifiedHeightDp.toPx().roundToInt() }
|
||||
} else {
|
||||
(contentHeight + verticalPaddingPx + verticalBorderPx).roundToInt()
|
||||
|
|
@ -1000,6 +920,10 @@ private suspend fun splitParagraphBlock(
|
|||
): Pair<ParagraphBlock, ParagraphBlock>? {
|
||||
val text = block.content
|
||||
if (text.isEmpty()) return null
|
||||
val boxMetrics = computeBlockBoxMetrics(block, constraints, density)
|
||||
val paragraphConstraints = boxMetrics.contentConstraints
|
||||
val paragraphStyle = textStyle.copy(textAlign = block.textAlign ?: textStyle.textAlign)
|
||||
val centeredSafetyPaddingPx = centeredTextSafetyPaddingPx(paragraphStyle, density)
|
||||
|
||||
val decorationTop = with(density) {
|
||||
block.style.padding.top.toPx() + (block.style.borderTop?.width?.toPx() ?: 0f)
|
||||
|
|
@ -1009,7 +933,7 @@ private suspend fun splitParagraphBlock(
|
|||
block.style.padding.bottom.toPx() + (block.style.borderBottom?.width?.toPx() ?: 0f)
|
||||
}.roundToInt()
|
||||
|
||||
val availableTextHeight = availableHeight - decorationTop - decorationBottom
|
||||
val availableTextHeight = availableHeight - decorationTop - decorationBottom - centeredSafetyPaddingPx
|
||||
|
||||
Timber.tag("PAGINATION_DEBUG").d("SplitPara: totalAvail=$availableHeight, topDec=$decorationTop, botDec=$decorationBottom, textAvail=$availableTextHeight")
|
||||
|
||||
|
|
@ -1021,8 +945,8 @@ private suspend fun splitParagraphBlock(
|
|||
val layoutResult = withContext(Dispatchers.Main) {
|
||||
textMeasurer.measure(
|
||||
text = text,
|
||||
style = textStyle,
|
||||
constraints = constraints.copy(maxHeight = Constraints.Infinity)
|
||||
style = paragraphStyle,
|
||||
constraints = paragraphConstraints
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -1036,7 +960,7 @@ private suspend fun splitParagraphBlock(
|
|||
|
||||
var lastVisibleLine = layoutResult.getLineForVerticalPosition(availableTextHeight.toFloat())
|
||||
|
||||
if (layoutResult.getLineBottom(lastVisibleLine) > availableHeight.toFloat()) {
|
||||
if (layoutResult.getLineBottom(lastVisibleLine) > availableTextHeight.toFloat()) {
|
||||
lastVisibleLine--
|
||||
}
|
||||
|
||||
|
|
@ -1056,7 +980,8 @@ private suspend fun splitParagraphBlock(
|
|||
val part2Layout = withContext(Dispatchers.Main) {
|
||||
textMeasurer.measure(
|
||||
text = part2CheckText,
|
||||
constraints = constraints
|
||||
style = paragraphStyle,
|
||||
constraints = paragraphConstraints
|
||||
)
|
||||
}
|
||||
if (part2Layout.lineCount == 1) {
|
||||
|
|
@ -1149,11 +1074,12 @@ private suspend fun calculateContentHeightWithMargins(
|
|||
constraints: Constraints,
|
||||
defaultStyle: TextStyle,
|
||||
headerStyle: TextStyle,
|
||||
density: Density
|
||||
density: Density,
|
||||
imageSizeMultiplier: Float = 1.0f
|
||||
): Int {
|
||||
var totalHeight = 0
|
||||
children.forEachIndexed { index, child ->
|
||||
val childHeight = measureBlockHeight(child, textMeasurer, constraints, defaultStyle, headerStyle, density)
|
||||
val childHeight = measureBlockHeight(child, textMeasurer, constraints, defaultStyle, headerStyle, density, imageSizeMultiplier)
|
||||
val margin = with(density) {
|
||||
if (index > 0) {
|
||||
val prevMargin = children[index - 1].style.margin.bottom.toPx()
|
||||
|
|
@ -1172,6 +1098,117 @@ private suspend fun calculateContentHeightWithMargins(
|
|||
return totalHeight
|
||||
}
|
||||
|
||||
private data class BlockBoxMetrics(
|
||||
val verticalPaddingPx: Float,
|
||||
val verticalBorderPx: Float,
|
||||
val contentConstraints: Constraints
|
||||
)
|
||||
|
||||
private fun computeBlockBoxMetrics(
|
||||
block: ContentBlock,
|
||||
constraints: Constraints,
|
||||
density: Density
|
||||
): BlockBoxMetrics {
|
||||
val verticalPaddingPx: Float
|
||||
val horizontalPaddingPx: Float
|
||||
val verticalBorderPx: Float
|
||||
val horizontalBorderPx: Float
|
||||
|
||||
with(density) {
|
||||
verticalPaddingPx = block.style.padding.top.toPx() + block.style.padding.bottom.toPx()
|
||||
horizontalPaddingPx = block.style.padding.left.toPx() + block.style.padding.right.toPx()
|
||||
verticalBorderPx = (block.style.borderTop?.width?.toPx() ?: 0f) + (block.style.borderBottom?.width?.toPx() ?: 0f)
|
||||
horizontalBorderPx = (block.style.borderLeft?.width?.toPx() ?: 0f) + (block.style.borderRight?.width?.toPx() ?: 0f)
|
||||
}
|
||||
|
||||
val isBorderBox = block.style.boxSizing == "border-box"
|
||||
val specifiedWidthDp = block.style.width
|
||||
val specifiedMaxWidthDp = block.style.maxWidth
|
||||
|
||||
val blockOuterWidthPx = with(density) {
|
||||
var effectiveWidthPx = constraints.maxWidth.toFloat()
|
||||
if (specifiedWidthDp != Dp.Unspecified) {
|
||||
effectiveWidthPx = specifiedWidthDp.toPx()
|
||||
}
|
||||
if (specifiedMaxWidthDp != Dp.Unspecified) {
|
||||
val maxWidthPx = specifiedMaxWidthDp.toPx()
|
||||
if (effectiveWidthPx > maxWidthPx) {
|
||||
effectiveWidthPx = maxWidthPx
|
||||
}
|
||||
}
|
||||
effectiveWidthPx.coerceAtMost(constraints.maxWidth.toFloat())
|
||||
}
|
||||
|
||||
val contentMaxWidth = if (specifiedWidthDp == Dp.Unspecified || isBorderBox) {
|
||||
blockOuterWidthPx - horizontalPaddingPx - horizontalBorderPx
|
||||
} else {
|
||||
blockOuterWidthPx
|
||||
}
|
||||
|
||||
return BlockBoxMetrics(
|
||||
verticalPaddingPx = verticalPaddingPx,
|
||||
verticalBorderPx = verticalBorderPx,
|
||||
contentConstraints = constraints.copy(
|
||||
maxWidth = contentMaxWidth.roundToInt().coerceAtLeast(0),
|
||||
maxHeight = Constraints.Infinity
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun centeredTextSafetyPaddingPx(
|
||||
style: TextStyle,
|
||||
density: Density
|
||||
): Int {
|
||||
if (style.textAlign != androidx.compose.ui.text.style.TextAlign.Center) return 0
|
||||
|
||||
val fallbackLineHeight = if (style.fontSize.isSpecified) {
|
||||
style.fontSize * 1.2f
|
||||
} else {
|
||||
16.sp * 1.2f
|
||||
}
|
||||
val effectiveLineHeight = if (style.lineHeight.isSpecified) style.lineHeight else fallbackLineHeight
|
||||
|
||||
return with(density) { effectiveLineHeight.toPx().roundToInt() }
|
||||
}
|
||||
|
||||
private fun measureScaledImageHeightPx(
|
||||
block: ImageBlock,
|
||||
density: Density,
|
||||
contentMaxWidth: Float,
|
||||
imageSizeMultiplier: Float
|
||||
): Float? = measureScaledImageSizePx(
|
||||
block = block,
|
||||
density = density,
|
||||
maxWidthPx = contentMaxWidth,
|
||||
imageSizeMultiplier = imageSizeMultiplier
|
||||
).second.takeIf { it > 0f }
|
||||
|
||||
private fun measureScaledImageSizePx(
|
||||
block: ImageBlock,
|
||||
density: Density,
|
||||
maxWidthPx: Float,
|
||||
imageSizeMultiplier: Float
|
||||
): Pair<Float, Float> {
|
||||
val intrinsicWidth = block.intrinsicWidth
|
||||
val intrinsicHeight = block.intrinsicHeight
|
||||
if (intrinsicWidth == null || intrinsicHeight == null || intrinsicWidth <= 0f || intrinsicHeight <= 0f) {
|
||||
return 0f to 0f
|
||||
}
|
||||
|
||||
val aspectRatio = intrinsicHeight / intrinsicWidth
|
||||
val baseWidth = with(density) {
|
||||
if (block.style.width.isSpecified) block.style.width.toPx() else maxWidthPx
|
||||
}
|
||||
|
||||
var scaledWidth = baseWidth * imageSizeMultiplier
|
||||
if (block.style.maxWidth.isSpecified) {
|
||||
scaledWidth = scaledWidth.coerceAtMost(with(density) { block.style.maxWidth.toPx() } * imageSizeMultiplier)
|
||||
}
|
||||
scaledWidth = scaledWidth.coerceAtMost(maxWidthPx)
|
||||
|
||||
return scaledWidth to (scaledWidth * aspectRatio)
|
||||
}
|
||||
|
||||
private fun zeroOutBottomMargin(blocks: MutableList<ContentBlock>) {
|
||||
if (blocks.isNotEmpty()) {
|
||||
val lastBlock = blocks.last()
|
||||
|
|
@ -1180,4 +1217,4 @@ private fun zeroOutBottomMargin(blocks: MutableList<ContentBlock>) {
|
|||
setBlockExpectedHeight(copyBlockWithNewStyle(lastBlock, newLastStyle), lastBlock.expectedHeight)
|
||||
blocks[blocks.size - 1] = newLastBlock
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -198,7 +198,7 @@ abstract class BookCacheDao {
|
|||
ConfigurationCache::class,
|
||||
AnchorIndexEntry::class
|
||||
],
|
||||
version = 7,
|
||||
version = 8,
|
||||
exportSchema = false
|
||||
)
|
||||
abstract class BookCacheDatabase : RoomDatabase() {
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import androidx.room.ForeignKey
|
|||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
const val LATEST_PROCESSING_VERSION = 7
|
||||
const val LATEST_PROCESSING_VERSION = 8
|
||||
|
||||
@Entity(tableName = "processed_books")
|
||||
data class ProcessedBook(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue