From 028685d62bae82a3595e2b6418eda85f48d635a9 Mon Sep 17 00:00:00 2001 From: austinried <4966622+austinried@users.noreply.github.com> Date: Mon, 19 Oct 2026 13:47:43 +0000 Subject: [PATCH] ui: show three lines of the collapsed artist bio --- .../ui/library/ArtistDetailScreen.kt | 58 +++++++++++-------- .../ui/library/ArtistDetailScreenTest.kt | 30 ++++++++++ 2 files changed, 63 insertions(+), 25 deletions(-) diff --git a/app/src/main/kotlin/com/subtracks/ui/library/ArtistDetailScreen.kt b/app/src/main/kotlin/com/subtracks/ui/library/ArtistDetailScreen.kt index 07723b86..ace37164 100644 --- a/app/src/main/kotlin/com/subtracks/ui/library/ArtistDetailScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/library/ArtistDetailScreen.kt @@ -584,7 +584,7 @@ private fun ArtistBio( remember(text, fullWidth, reserve, reserveLines, collapsed, expanded, moreText, lessText) { when { collapsed -> { - splitBioCollapsed(measurer, text, style, fullWidth, reserve, moreText) + splitBioCollapsed(measurer, text, style, fullWidth, reserve, moreText, maxLines = 3) } expanded -> { @@ -772,7 +772,7 @@ internal data class BioLines( val less: Boolean = false, ) -/** Collapses text to two lines, appending a bold More only when it does not fit. */ +/** Collapses text to [maxLines] lines, appending a bold More only when it does not fit. */ internal fun splitBioCollapsed( measurer: TextMeasurer, text: String, @@ -780,6 +780,7 @@ internal fun splitBioCollapsed( fullWidth: Int, reserve: Int, moreText: String, + maxLines: Int = 2, ): BioLines { val words = text @@ -789,26 +790,33 @@ internal fun splitBioCollapsed( .filter { it.isNotEmpty() } if (words.isEmpty()) return BioLines(listOf(AnnotatedString("")), more = false) val available = (fullWidth - reserve).coerceAtLeast(1) - var first = "" + val lines = mutableListOf() var index = 0 - while (index < words.size) { - val candidate = if (first.isEmpty()) words[index] else "$first ${words[index]}" - if (measurer.measure(AnnotatedString(candidate), style, maxLines = 1).size.width <= available) { - first = candidate - index++ - } else if (first.isEmpty()) { - first = words[index] - index++ - } else { - break + while (index < words.size && lines.size < maxLines - 1) { + var current = "" + while (index < words.size) { + val candidate = if (current.isEmpty()) words[index] else "$current ${words[index]}" + if (measurer.measure(AnnotatedString(candidate), style, maxLines = 1).size.width <= available) { + current = candidate + index++ + } else if (current.isEmpty()) { + current = words[index] + index++ + break + } else { + break + } } + lines += AnnotatedString(current) + } + if (index >= words.size) { + return BioLines(lines, more = false) } - if (index >= words.size) return BioLines(listOf(AnnotatedString(first)), more = false) - val secondStart = index - val remaining = words.drop(secondStart).joinToString(" ") + val remaining = words.drop(index).joinToString(" ") if (measurer.measure(AnnotatedString(remaining), style, maxLines = 1).size.width <= available) { - return BioLines(listOf(AnnotatedString(first), AnnotatedString(remaining)), more = false) + lines += AnnotatedString(remaining) + return BioLines(lines, more = false) } val more = @@ -819,25 +827,25 @@ internal fun splitBioCollapsed( val moreWidth = measurer.measure(more, style, maxLines = 1).size.width + measurer.measure(AnnotatedString(" "), style, maxLines = 1).size.width - val secondAvailable = (available - moreWidth).coerceAtLeast(1) + val lastAvailable = (available - moreWidth).coerceAtLeast(1) var truncated = "" - var truncatedIndex = secondStart - while (truncatedIndex < words.size) { - val candidate = if (truncated.isEmpty()) words[truncatedIndex] else "$truncated ${words[truncatedIndex]}" - if (measurer.measure(AnnotatedString(candidate), style, maxLines = 1).size.width <= secondAvailable) { + while (index < words.size) { + val candidate = if (truncated.isEmpty()) words[index] else "$truncated ${words[index]}" + if (measurer.measure(AnnotatedString(candidate), style, maxLines = 1).size.width <= lastAvailable) { truncated = candidate - truncatedIndex++ + index++ } else { break } } - val secondLine = + val lastLine = buildAnnotatedString { if (truncated.isNotEmpty()) append("$truncated ") append("… ") withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { append(moreText) } } - return BioLines(listOf(AnnotatedString(first), secondLine), more = true) + lines += lastLine + return BioLines(lines, more = true) } @Composable diff --git a/app/src/test/kotlin/com/subtracks/ui/library/ArtistDetailScreenTest.kt b/app/src/test/kotlin/com/subtracks/ui/library/ArtistDetailScreenTest.kt index c8e9f5fd..a7e2cb2e 100644 --- a/app/src/test/kotlin/com/subtracks/ui/library/ArtistDetailScreenTest.kt +++ b/app/src/test/kotlin/com/subtracks/ui/library/ArtistDetailScreenTest.kt @@ -257,6 +257,36 @@ class ArtistDetailScreenTest { ) } + @Test + fun theCollapsedPreviewCanShowThreeLines() { + val bio = List(6) { "This is a sufficiently long biography sentence." }.joinToString(" ") + var result: BioLines? = null + composeRule.setContent { + val measurer = rememberTextMeasurer() + result = + splitBioCollapsed( + measurer, + bio, + TextStyle(fontSize = 14.sp), + 400, + 0, + "More", + maxLines = 3, + ) + } + composeRule.waitForIdle() + + val lines = requireNotNull(result) + assertEquals(3, lines.lines.size) + assertTrue(lines.more) + assertTrue( + lines.lines + .last() + .text + .endsWith("More"), + ) + } + @Test fun anOversizedFirstWordIsStillShown() { var result: BioLines? = null