diff --git a/app/src/main/java/com/example/research/ui/article/ArticleScreen.kt b/app/src/main/java/com/example/research/ui/article/ArticleScreen.kt index c799827..b91f4fe 100644 --- a/app/src/main/java/com/example/research/ui/article/ArticleScreen.kt +++ b/app/src/main/java/com/example/research/ui/article/ArticleScreen.kt @@ -18,6 +18,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.unit.dp +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.LineHeightStyle import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.IntSize import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -48,6 +51,54 @@ fun ArticleRoute( ) } +private const val MERGED_BLOCK_MAX_CHARS = 1500 +private const val MERGED_BLOCK_MAX_ITEMS = 12 + +private fun mergeDslBlocks(blocks: List): List { + if (blocks.size <= MERGED_BLOCK_MAX_ITEMS) return blocks + val merged = ArrayList(blocks.size / 4) + var i = 0 + while (i < blocks.size) { + val builder = AnnotatedString.Builder() + val refs = ArrayList() + var j = i + while (j < blocks.size) { + val block = blocks[j] + repeat(block.indent) { builder.append(' ') } + val paraStart = builder.length + builder.append(block.text) + block.lazyRefs.forEach { ref -> + refs.add( + ref.copy( + startIndex = ref.startIndex + paraStart, + endIndex = ref.endIndex + paraStart + ) + ) + } + val more = j + 1 < blocks.size && + (j + 1 - i) < MERGED_BLOCK_MAX_ITEMS && + builder.length < MERGED_BLOCK_MAX_CHARS + if (more) { + builder.append('\n') + } + j++ + if (!more) { + break + } + } + merged.add( + DslBlock( + text = builder.toAnnotatedString(), + indent = 0, + lazyRefs = refs, + continuesPrevious = blocks[i].continuesPrevious + ) + ) + i = j + } + return merged +} + @Composable fun ArticleContent( articleState: ArticleState?, @@ -63,7 +114,11 @@ fun ArticleContent( is ArticleState.Success -> { val articleSaveKey = "${articleState.dictionaryPath}:${articleState.title}" val baseTextStyle = MaterialTheme.typography.bodyLarge.copy( - color = MaterialTheme.colorScheme.onSurface + color = MaterialTheme.colorScheme.onSurface, + lineHeightStyle = LineHeightStyle( + alignment = LineHeightStyle.Alignment.Proportional, + trim = LineHeightStyle.Trim.Both, + ), ) val indentStep = 16.dp @@ -123,6 +178,12 @@ fun ArticleContent( val isHeavyArticle = articleState.dslContent.size > HEAVY_ARTICLE_BLOCK_COUNT + val displayBlocks = remember( + articleState.dslContent, + baseTextStyle + ) { + mergeDslBlocks(articleState.dslContent) + } val articleList: @Composable (Modifier) -> Unit = { listModifier -> LazyColumn( state = listState, @@ -134,39 +195,30 @@ fun ArticleContent( } ) { itemsIndexed( - items = articleState.dslContent, + items = displayBlocks, key = { index, _ -> - // Use stable key combining article save key and index "$articleSaveKey:$index" }, contentType = { _, _ -> "article_block" } ) { index, block -> - val followedByContinuation = - articleState.dslContent.getOrNull(index + 1)?.continuesPrevious == true - val textContent = @Composable { - Box( + Box( + modifier = Modifier + .padding(start = Spacing.contentStart + indentStep * block.indent) + ) { + ArticleLinkText( + block = block, + linksEnabled = true, + preferDirectHitTesting = isHeavyArticle, + style = baseTextStyle, + onRefClick = refClickHandler, + onTextLayout = {}, modifier = Modifier - .padding( - start = Spacing.contentStart + indentStep * block.indent - ) - .padding(bottom = if (followedByContinuation) 0.dp else 8.dp) - ) { - ArticleLinkText( - block = block, - linksEnabled = true, - preferDirectHitTesting = isHeavyArticle, - style = baseTextStyle, - onRefClick = refClickHandler, - onTextLayout = {}, - modifier = Modifier - .testTag("article_block_$index") - .semantics { - testTagsAsResourceId = true - } - ) - } + .testTag("article_block_$index") + .semantics { + testTagsAsResourceId = true + } + ) } - textContent() } } }