summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-11-01 08:47:20 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-11-01 08:47:20 +0000
commitdc4a7dd45fd9895bf4952c95fdc93c1b0786d52b (patch)
treec52778380cf79b8da9374d797d0d16a828e940df
parent384f7d224f73b69b099f060d0d46708a9bd47c58 (diff)
A transcript line says when it was said
Reviewing the transcript screen on a device: a wall of paragraphs with no times in it. That undercuts the whole reason the screen exists -- finding the bit you half-heard at twelve minutes -- and leaves a reader no sign that a line can be tapped at all, since plain text suggests nothing. A fixed gutter so the times form a column instead of ragging in and out with the words beside them, wide enough for "1:02:03" so an hour-long episode does not shunt the text sideways. Quieter than the words, because it orients rather than being the thing read. Tapping it seeks, like tapping the words: the line is one target. formatClock rather than a fourth time formatter -- this app already had three. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B
-rw-r--r--app/src/androidTest/java/org/terst/listen/ui/TranscriptCueListTest.kt39
-rw-r--r--app/src/main/java/org/terst/listen/ui/TranscriptScreen.kt66
2 files changed, 89 insertions, 16 deletions
diff --git a/app/src/androidTest/java/org/terst/listen/ui/TranscriptCueListTest.kt b/app/src/androidTest/java/org/terst/listen/ui/TranscriptCueListTest.kt
index ba1cbde..5c2c7b6 100644
--- a/app/src/androidTest/java/org/terst/listen/ui/TranscriptCueListTest.kt
+++ b/app/src/androidTest/java/org/terst/listen/ui/TranscriptCueListTest.kt
@@ -69,4 +69,43 @@ class TranscriptCueListTest {
compose.onNodeWithText("Welcome back to the show.").assertDoesNotExist()
}
+
+ /*
+ * The time each line was said.
+ *
+ * The reason a transcript is worth scrolling: without it there is no way to find the bit
+ * you half-heard at twelve minutes, which is what people open a transcript for. It is also
+ * the only sign a line can be tapped -- plain paragraphs suggest nothing.
+ */
+
+ @Test
+ fun everyLineSaysWhenItWasSaid() {
+ compose.setContent { CueList(cues, currentIndex = null) {} }
+
+ compose.onNodeWithText("0:00").assertIsDisplayed()
+ compose.onNodeWithText("0:03").assertIsDisplayed()
+ compose.onNodeWithText("0:20").assertIsDisplayed()
+ }
+
+ /** An hour-long episode's times must not shunt the words sideways. */
+ @Test
+ fun anHourLongEpisodeShowsHoursInTheTime() {
+ val late = listOf(
+ TranscriptCueEntity("e1", startMs = 3_723_000, endMs = 3_726_000, text = "Late on."),
+ )
+ compose.setContent { CueList(late, currentIndex = null) {} }
+
+ compose.onNodeWithText("1:02:03").assertIsDisplayed()
+ }
+
+ /** Tapping the time seeks, the same as tapping the words: the whole line is the target. */
+ @Test
+ fun tappingTheTimeSeeksToo() {
+ var sought: Long? = null
+ compose.setContent { CueList(cues, currentIndex = null) { sought = it } }
+
+ compose.onNodeWithText("0:03").performClick()
+
+ assertEquals(3500L, sought)
+ }
}
diff --git a/app/src/main/java/org/terst/listen/ui/TranscriptScreen.kt b/app/src/main/java/org/terst/listen/ui/TranscriptScreen.kt
index 81c1b84..9a32a7e 100644
--- a/app/src/main/java/org/terst/listen/ui/TranscriptScreen.kt
+++ b/app/src/main/java/org/terst/listen/ui/TranscriptScreen.kt
@@ -3,7 +3,9 @@ package org.terst.listen.ui
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
@@ -103,6 +105,12 @@ fun TranscriptScreen(
/** Advertising is dimmed, not hidden: a wrong call has to stay visible to be disagreed with. */
private const val ADVERTISING_ALPHA = 0.45f
+/** Quieter than the words it points at -- it orients, it is not the thing being read. */
+private const val TIME_ALPHA = 0.7f
+
+/** Wide enough for "1:02:03", so an hour-long episode does not shunt the text sideways. */
+private val TIME_GUTTER = 64.dp
+
@Composable
internal fun CueList(
cues: List<TranscriptCueEntity>,
@@ -118,25 +126,51 @@ internal fun CueList(
LazyColumn(state = listState, modifier = Modifier.fillMaxSize()) {
items(cues, key = { it.startMs }) { cue ->
val speaking = currentIndex != null && cues[currentIndex].startMs == cue.startMs
- Text(
- cue.text,
- style = MaterialTheme.typography.bodyMedium,
- fontWeight = if (speaking) FontWeight.Bold else FontWeight.Normal,
+ TranscriptLine(cue = cue, speaking = speaking, onSeek = onSeek)
+ }
+ }
+}
+
+/**
+ * One line, with the time it was said.
+ *
+ * The time is the whole reason a transcript is worth scrolling: without it there is no way to
+ * find the bit you half-heard at twelve minutes, which is the thing people actually open a
+ * transcript for. It doubles as the only sign that a line can be tapped at all -- plain
+ * paragraphs give a reader nothing to suggest they seek.
+ *
+ * A fixed gutter so the times form a column rather than ragging in and out with the text
+ * beside them.
+ */
+@Composable
+private fun TranscriptLine(cue: TranscriptCueEntity, speaking: Boolean, onSeek: (Long) -> Unit) {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .clickable { onSeek(cue.startMs) }
+ .padding(horizontal = 16.dp, vertical = 6.dp),
+ ) {
+ Text(
+ formatClock(cue.startMs),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = TIME_ALPHA),
+ modifier = Modifier.width(TIME_GUTTER),
+ )
+ Text(
+ cue.text,
+ style = MaterialTheme.typography.bodyMedium,
+ fontWeight = if (speaking) FontWeight.Bold else FontWeight.Normal,
// Advertising is dimmed rather than hidden or struck through. The reader can
// see what the server thinks is an advertisement and disagree with it, which
// matters because nothing has ever checked this detector against anything --
// and hiding the lines would make a wrong call invisible.
- color = when {
- cue.advertising -> MaterialTheme.colorScheme.onSurfaceVariant
- .copy(alpha = ADVERTISING_ALPHA)
- speaking -> MaterialTheme.colorScheme.onSurface
- else -> MaterialTheme.colorScheme.onSurfaceVariant
- },
- modifier = Modifier
- .fillMaxWidth()
- .clickable { onSeek(cue.startMs) }
- .padding(horizontal = 16.dp, vertical = 6.dp),
- )
- }
+ color = when {
+ cue.advertising -> MaterialTheme.colorScheme.onSurfaceVariant
+ .copy(alpha = ADVERTISING_ALPHA)
+ speaking -> MaterialTheme.colorScheme.onSurface
+ else -> MaterialTheme.colorScheme.onSurfaceVariant
+ },
+ modifier = Modifier.weight(1f),
+ )
}
}