Recently Written · git

subread-overlay

Shows the lines of an .srt over any Android media player, in time with it. Tap a word to look it up.

git clone https://github.com/equwal/subread-overlay

Log | Files | Refs


commit dd1d36137139fd501997f8f986eaa9cd8eb2f4b0
equwal <truex@equwal.com>
2026-09-22 22:47:59 -0700

Look up a word when the finger lifts from it

A touch on a word pauses the player at once. Before, the player ran on
until the user pressed "Look up", and the line could go past.

When the finger lifts, the selection goes to the dictionary. A drag
before the lift selects more words. The "Look up" button is removed.
A touch beside the words does not pause the player.

 README.md                                          |  6 +--
 .../kotlin/space/subread/overlay/OverlayTest.kt    | 52 +++++++++++++++++++---
 .../kotlin/space/subread/overlay/MediaListener.kt  |  4 ++
 .../kotlin/space/subread/overlay/OverlayView.kt    | 17 ++++---
 .../metadata/android/en-US/full_description.txt    |  2 +-
 5 files changed, 67 insertions(+), 14 deletions(-)
diff --git a/README.md b/README.md
index e7b8f46..ac0c3d1 100644
--- a/README.md
+++ b/README.md
@@ -31,9 +31,9 @@ whether it shows the line before and the line after the line of now.
 On the panel:
 
 - `≡` moves the panel.
-- A tap selects the word under the finger. A drag selects more words.
-  "Look up" pauses the player and sends the selection to the dictionary.
-  "Share" opens the share sheet of Android, for an app without an entry in
+- A tap on a word pauses the player at once and selects the word. A drag
+  selects more words. When the finger lifts, the selection goes to the
+  dictionary. "Share" opens the share sheet of Android, for an app without an entry in
   the text selection menu. "Copy" copies it.
 - `▶` starts the player again after a lookup. `⏸` pauses it.
 - `⋯` opens the timing row. `−0.5 s` and `+0.5 s` shift the subtitles.
diff --git a/app/src/androidTest/kotlin/space/subread/overlay/OverlayTest.kt b/app/src/androidTest/kotlin/space/subread/overlay/OverlayTest.kt
index 27bfb31..2b6647b 100644
--- a/app/src/androidTest/kotlin/space/subread/overlay/OverlayTest.kt
+++ b/app/src/androidTest/kotlin/space/subread/overlay/OverlayTest.kt
@@ -81,11 +81,13 @@ class WordsTest {
 class OverlayViewTest {
 
     private val lookedUp = CopyOnWriteArrayList<String>()
+    private var touches = 0
     private val shared = CopyOnWriteArrayList<String>()
     private val events = object : OverlayView.Events {
         override fun onDrag(dx: Float, dy: Float) = Unit
         override fun onDragEnd() = Unit
         override fun onClose() = Unit
+        override fun onTouchWord() { touches++ }
         override fun onLookUp(word: String) { lookedUp += word }
         override fun onShare(word: String) { shared += word }
         override fun onTogglePlay() = Unit
@@ -103,8 +105,52 @@ class OverlayViewTest {
     private fun OverlayView.rowOf(offset: Int): Float =
         text.totalPaddingTop + text.layout.getLineBaseline(text.layout.getLineForOffset(offset)) - 5f
 
+    /** Gives a touch event at [x], [y] of the text view to the text view. */
+    private fun OverlayView.touch(action: Int, x: Float, y: Float) {
+        val now = SystemClock.uptimeMillis()
+        val event = android.view.MotionEvent.obtain(now, now, action, x, y, 0)
+        text.dispatchTouchEvent(event)
+        event.recycle()
+    }
+
     @Test
-    fun aTapSelectsTheWordUnderTheFingerAndLookUpSendsIt() {
+    fun aTouchPausesAtOnceAndTheLiftLooksUpTheSelection() {
+        ActivityScenario.launch(MainActivity::class.java).use { scenario ->
+            lateinit var panel: OverlayView
+            scenario.onActivity {
+                panel = OverlayView(it, events)
+                panel.setTextSize(30f)
+                it.addContentView(panel, ViewGroup.LayoutParams(-1, -2))
+                panel.showLine("reading along is easy")
+            }
+            InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+            scenario.onActivity {
+                val start = panel.line.indexOf("along")
+                val last = panel.line.lastIndex
+                panel.touch(android.view.MotionEvent.ACTION_DOWN, panel.middleOf(start + 2), panel.rowOf(start + 2))
+                assertEquals("the finger on a word pauses the player", 1, touches)
+                assertEquals("no lookup before the finger lifts", emptyList<String>(), lookedUp)
+                panel.touch(android.view.MotionEvent.ACTION_MOVE, panel.middleOf(last), panel.rowOf(last))
+                panel.touch(android.view.MotionEvent.ACTION_UP, panel.middleOf(last), panel.rowOf(last))
+                assertEquals(listOf("along is easy"), lookedUp)
+
+                val found = arrayListOf<android.view.View>()
+                panel.findViewsWithText(found, "Look up", android.view.View.FIND_VIEWS_WITH_TEXT)
+                assertEquals("no \"Look up\" button", 0, found.size)
+
+                // A tap beside the words neither pauses nor looks up.
+                val right = panel.text.width - 1f
+                panel.touch(android.view.MotionEvent.ACTION_DOWN, right, panel.rowOf(last))
+                panel.touch(android.view.MotionEvent.ACTION_UP, right, panel.rowOf(last))
+                assertNull(panel.selection)
+                assertEquals(1, touches)
+                assertEquals(listOf("along is easy"), lookedUp)
+            }
+        }
+    }
+
+    @Test
+    fun aTapSelectsTheWordUnderTheFingerAndADragSelectsMore() {
         ActivityScenario.launch(MainActivity::class.java).use { scenario ->
             lateinit var panel: OverlayView
             scenario.onActivity {
@@ -126,10 +172,6 @@ class OverlayViewTest {
                 assertEquals(start..last, panel.selection)
 
                 val found = arrayListOf<android.view.View>()
-                panel.findViewsWithText(found, "Look up", android.view.View.FIND_VIEWS_WITH_TEXT)
-                found.single().performClick()
-                assertEquals(listOf("along is easy"), lookedUp)
-                found.clear()
                 panel.findViewsWithText(found, "Share", android.view.View.FIND_VIEWS_WITH_TEXT)
                 found.single().performClick()
                 assertEquals(listOf("along is easy"), shared)
diff --git a/app/src/main/kotlin/space/subread/overlay/MediaListener.kt b/app/src/main/kotlin/space/subread/overlay/MediaListener.kt
index 252eb80..6759612 100644
--- a/app/src/main/kotlin/space/subread/overlay/MediaListener.kt
+++ b/app/src/main/kotlin/space/subread/overlay/MediaListener.kt
@@ -206,6 +206,10 @@ class MediaListener : NotificationListenerService(), OverlayView.Events {
     }
 
     /** The player pauses for the lookup. The play button of the panel starts it again. */
+    override fun onTouchWord() {
+        follower.pause()
+    }
+
     override fun onLookUp(word: String) {
         follower.pause()
         runCatching { startActivity(Lookup.intent(word, store.dictionary)) }.onFailure {
diff --git a/app/src/main/kotlin/space/subread/overlay/OverlayView.kt b/app/src/main/kotlin/space/subread/overlay/OverlayView.kt
index 540d811..62fd35f 100644
--- a/app/src/main/kotlin/space/subread/overlay/OverlayView.kt
+++ b/app/src/main/kotlin/space/subread/overlay/OverlayView.kt
@@ -23,9 +23,10 @@ import androidx.core.view.isVisible
 /**
  * The panel over the media player: the subtitle line, and a few buttons.
  *
- * A tap on the line selects the word under the finger. A drag makes the selection longer, word
- * by word. The selection goes to a dictionary with "Look up". The system text selection is not
- * used: it needs a window with the input focus, and then the player below gets no keys.
+ * A tap on the line selects the word under the finger and pauses the player at once. A drag
+ * makes the selection longer, word by word. When the finger lifts, the selection goes to the
+ * dictionary. The system text selection is not used: it needs a window with the input focus,
+ * and then the player below gets no keys.
  *
  * Black on white and nothing that moves, so that the panel is usable on an e-ink screen.
  */
@@ -36,6 +37,9 @@ class OverlayView(context: Context, private val events: Events) : LinearLayout(c
         fun onDrag(dx: Float, dy: Float)
         fun onDragEnd()
         fun onClose()
+        /** A finger is on a word: the player pauses now, before the lookup. */
+        fun onTouchWord()
+        /** The finger lifted from a selection: the selection goes to the dictionary. */
         fun onLookUp(word: String)
         /** "Share": the selection goes to an app that the user picks in the share sheet. */
         fun onShare(word: String)
@@ -90,7 +94,6 @@ class OverlayView(context: Context, private val events: Events) : LinearLayout(c
         lookUpRow.gravity = Gravity.END
         lookUpRow.addView(button("Copy", null) { copy() }, wrap())
         lookUpRow.addView(button("Share", null) { share() }, wrap())
-        lookUpRow.addView(button("Look up", null) { lookUp() }, wrap())
         lookUpRow.visibility = View.GONE
         addView(lookUpRow, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
 
@@ -212,8 +215,12 @@ class OverlayView(context: Context, private val events: Events) : LinearLayout(c
     private fun selectWith(view: TextView) {
         view.setOnTouchListener { _, event ->
             when (event.actionMasked) {
-                MotionEvent.ACTION_DOWN -> selectAt(event.x, event.y)
+                MotionEvent.ACTION_DOWN -> {
+                    selectAt(event.x, event.y)
+                    if (selection != null) events.onTouchWord()
+                }
                 MotionEvent.ACTION_MOVE -> selectAt(event.x, event.y, extend = true)
+                MotionEvent.ACTION_UP -> lookUp()
             }
             true
         }
diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt
index 1147b32..59bb9c0 100644
--- a/fastlane/metadata/android/en-US/full_description.txt
+++ b/fastlane/metadata/android/en-US/full_description.txt
@@ -3,7 +3,7 @@ SubRead Overlay shows the lines of a subtitle file (.srt) over the app that play
 <b>What it does</b>
 
 * Works with each player that publishes an Android media session: Voice, VLC, mpv-android, podcast apps and more. No player needs a change.
-* Tap a word to select it. Drag to select more words. "Look up" pauses the player and sends the selection to a dictionary, a translator or a flash card app. Each app with an entry in the text selection menu can be the target. "Share" opens the share sheet of Android for the other apps. A button on the panel starts the player again.
+* Tap a word: the player pauses at once, and the word goes to a dictionary, a translator or a flash card app. Drag to look up more words. Each app with an entry in the text selection menu can be the target. "Share" opens the share sheet of Android for the other apps. A button on the panel starts the player again.
 * On request the panel shows the line before and the line after too, and lets the player show through.
 * The line stays on the panel until the next line starts, so there is time to look a word up.
 * A timing row shifts the subtitles by half a second, or makes the line before or the next line the line of now.