From cb4a10beae2248cd77311ecfa2fca0f7d8d7e257 Mon Sep 17 00:00:00 2001 From: ecwilson Date: Tue, 24 Mar 2026 13:45:17 -0700 Subject: [PATCH 1/3] Fix: Keep text visible after WPM auto-scroll reaches the end In Classic and Voice-Activated modes, the overlay would immediately switch to a "Done" screen and auto-dismiss 1 second after the auto-scroll reached the last word. This made timed mode unusable because speakers typically finish talking 10-20 seconds after the scroll ends. Now in timer-based modes (Classic/Voice-Activated), when the scroll reaches the end on the last page: - The prompter text stays visible instead of switching to "Done" - The overlay does not auto-dismiss - The speaker can close manually via the X button or Esc key Word Tracking mode behavior is unchanged (auto-dismiss is appropriate there since it knows when the speaker actually finishes). Fixes f/textream#29 --- .../Textream/NotchOverlayController.swift | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/Textream/Textream/NotchOverlayController.swift b/Textream/Textream/NotchOverlayController.swift index 330d202..c4f5605 100644 --- a/Textream/Textream/NotchOverlayController.swift +++ b/Textream/Textream/NotchOverlayController.swift @@ -721,7 +721,7 @@ struct NotchOverlayView: View { if content.showPagePicker { pagePickerView - } else if isDone { + } else if isDone && (listeningMode == .wordTracking || hasNextPage) { doneView } else { prompterView @@ -765,12 +765,19 @@ struct NotchOverlayView: View { .animation(.easeInOut(duration: 0.5), value: isDone) .onChange(of: isDone) { _, done in if done { - // Stop listening when page is done - speechRecognizer.stop() + // In word tracking mode, stop listening when page is done + if listeningMode == .wordTracking { + speechRecognizer.stop() + } if !hasNextPage { - // Show "Done" briefly, then auto-dismiss - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - speechRecognizer.shouldDismiss = true + // Only auto-dismiss in word tracking mode. + // In classic/silence-paused modes the speaker may still be + // talking after the auto-scroll finishes, so keep the text + // visible and let them dismiss manually (X button or Esc). + if listeningMode == .wordTracking { + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + speechRecognizer.shouldDismiss = true + } } } else if NotchSettings.shared.autoNextPage { startCountdown() @@ -1213,7 +1220,7 @@ struct FloatingOverlayView: View { VStack(spacing: 0) { if content.showPagePicker { floatingPagePickerView - } else if isDone { + } else if isDone && (listeningMode == .wordTracking || hasNextPage) { floatingDoneView } else { floatingPrompterView @@ -1260,11 +1267,14 @@ struct FloatingOverlayView: View { .animation(.easeInOut(duration: 0.5), value: isDone) .onChange(of: isDone) { _, done in if done { - // Stop listening when page is done - speechRecognizer.stop() + if listeningMode == .wordTracking { + speechRecognizer.stop() + } if !hasNextPage { - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - speechRecognizer.shouldDismiss = true + if listeningMode == .wordTracking { + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + speechRecognizer.shouldDismiss = true + } } } else if followingCursor || NotchSettings.shared.autoNextPage { startCountdown() From 41ccdec68f0ff9fbabbc448cbebd0905f97b7c16 Mon Sep 17 00:00:00 2001 From: ecwilson Date: Tue, 24 Mar 2026 14:02:30 -0700 Subject: [PATCH 2/3] Apply same fix to ExternalDisplayView and BrowserServer Address code review findings: - ExternalDisplayView: gate doneView and speechRecognizer.stop() on wordTracking mode, matching NotchOverlayView/FloatingOverlayView - BrowserServer: suppress isDone in classic/silencePaused modes on last page so browser clients keep showing prompter text - Revert accidental DEVELOPMENT_TEAM change in project.pbxproj --- Textream/Textream/BrowserServer.swift | 5 ++++- Textream/Textream/ExternalDisplayController.swift | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Textream/Textream/BrowserServer.swift b/Textream/Textream/BrowserServer.swift index c58f481..70190ad 100644 --- a/Textream/Textream/BrowserServer.swift +++ b/Textream/Textream/BrowserServer.swift @@ -201,7 +201,10 @@ class BrowserServer { } let effective = min(charCount, totalCharCount) - let isDone = totalCharCount > 0 && effective >= totalCharCount + let rawDone = totalCharCount > 0 && effective >= totalCharCount + // In classic/silence-paused modes on the last page, suppress Done so the + // browser keeps showing the prompter text (speaker may still be talking). + let isDone = rawDone && (mode == .wordTracking || hasNextPage) let highlightWords = mode == .wordTracking diff --git a/Textream/Textream/ExternalDisplayController.swift b/Textream/Textream/ExternalDisplayController.swift index ac2b272..f239e63 100644 --- a/Textream/Textream/ExternalDisplayController.swift +++ b/Textream/Textream/ExternalDisplayController.swift @@ -176,7 +176,7 @@ struct ExternalDisplayView: View { ZStack { Color.black.ignoresSafeArea() - if isDone { + if isDone && (listeningMode == .wordTracking || hasNextPage) { doneView } else { prompterView @@ -192,7 +192,7 @@ struct ExternalDisplayView: View { .scaleEffect(x: mirrorAxis?.scaleX ?? 1, y: mirrorAxis?.scaleY ?? 1) .animation(.easeInOut(duration: 0.5), value: isDone) .onChange(of: isDone) { _, done in - if done { + if done && listeningMode == .wordTracking { speechRecognizer.stop() } } From cd53d977cf45388f9424a7ceefd96f72750e5a67 Mon Sep 17 00:00:00 2001 From: ecwilson Date: Tue, 24 Mar 2026 14:20:27 -0700 Subject: [PATCH 3/3] Stop BrowserServer timer from advancing after scroll completes The timerWordProgress was incrementing unboundedly after the scroll reached the end, unlike the SwiftUI views which guard with !isDone. Add a scrollDone check before incrementing to stop wasting CPU on the 100ms broadcast timer. --- Textream/Textream/BrowserServer.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Textream/Textream/BrowserServer.swift b/Textream/Textream/BrowserServer.swift index 70190ad..9b211fe 100644 --- a/Textream/Textream/BrowserServer.swift +++ b/Textream/Textream/BrowserServer.swift @@ -187,14 +187,18 @@ class BrowserServer { let charCount: Int let mode = NotchSettings.shared.listeningMode + // Check if scroll already reached the end, to stop advancing the timer + let scrollDone = totalCharCount > 0 && charOffsetForWordProgress(timerWordProgress) >= totalCharCount switch mode { case .wordTracking: charCount = speechRecognizer?.recognizedCharCount ?? 0 case .classic: - timerWordProgress += NotchSettings.shared.scrollSpeed * 0.1 + if !scrollDone { + timerWordProgress += NotchSettings.shared.scrollSpeed * 0.1 + } charCount = charOffsetForWordProgress(timerWordProgress) case .silencePaused: - if speechRecognizer?.isListening == true && (speechRecognizer?.isSpeaking ?? false) { + if !scrollDone && speechRecognizer?.isListening == true && (speechRecognizer?.isSpeaking ?? false) { timerWordProgress += NotchSettings.shared.scrollSpeed * 0.1 } charCount = charOffsetForWordProgress(timerWordProgress)