Recently Written · git

scan-subread

PDF reader that follows an audiobook with the SubRead suite

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

Log | Files | Refs


commit 41320e5f18cfac09a37dbcb553eefab57262d573
truex <truex@equwal.com>
2026-09-26 19:22:52 -0700

Say a player problem once in the strip

On the phone, play without notification access gave the same sentence
two times: in the player part and in the event part. A PlayerError for
the problem of the last player state now shows only in the player part
while a book is open (shownInPlayerPart). Before a book is open the
player part is hidden, so the event part still says it.

 src/main.ts           |  8 +++++++-
 src/messages.ts       |  9 +++++++++
 test/messages.test.ts | 18 ++++++++++++++++++
 3 files changed, 34 insertions(+), 1 deletion(-)
diff --git a/src/main.ts b/src/main.ts
index f3ab66a..f5090d3 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -36,6 +36,7 @@ import {
   playerMessage,
   readingEndMessage,
   readingText,
+  shownInPlayerPart,
   SUBREAD_NOT_INSTALLED,
   subreadErrorText,
   subreadQuestion,
@@ -281,8 +282,13 @@ function setForceOcr(on: boolean): void {
   showReadingPart();
 }
 
-/** Shows a command that failed in the strip. */
+/**
+ * Shows a command that failed in the strip. The player part shows the
+ * problem of the player while a book is open, so the event part does not
+ * say it again.
+ */
 function sayError(err: unknown): void {
+  if (state.pdf && shownInPlayerPart(err, state.clock?.error)) return;
   say(errorMessage(err, isAndroid, overlayInstalled()));
 }
 
diff --git a/src/messages.ts b/src/messages.ts
index b745df4..71010ab 100644
--- a/src/messages.ts
+++ b/src/messages.ts
@@ -103,6 +103,15 @@ export function errorMessage(err: unknown, android: boolean, overlayInstalled =
   return { text: String(err) };
 }
 
+/**
+ * True when `err` is a PlayerError for the problem of the last player state
+ * (`playerError`). The player part of the strip shows that problem, so the
+ * event part does not say it again.
+ */
+export function shownInPlayerPart(err: unknown, playerError: string | null | undefined): boolean {
+  return err instanceof PlayerError && err.reason === playerError;
+}
+
 /** The player part of the strip: the time and the play state, or the problem. */
 export function playerMessage(
   s: ClockState | null,
diff --git a/test/messages.test.ts b/test/messages.test.ts
index 98d160b..0a9e440 100644
--- a/test/messages.test.ts
+++ b/test/messages.test.ts
@@ -22,6 +22,7 @@ import {
   playerProblem,
   readingEndMessage,
   readingText,
+  shownInPlayerPart,
   subreadErrorText,
   subreadQuestion,
   subreadResultText,
@@ -80,6 +81,23 @@ describe('playerProblem', () => {
   });
 });
 
+describe('shownInPlayerPart', () => {
+  it('does not repeat the problem that the player part shows', () => {
+    // The phone finding: after play, the strip said the same sentence twice.
+    expect(
+      shownInPlayerPart(new PlayerError('no_notification_access'), 'no_notification_access'),
+    ).toBe(true);
+    expect(shownInPlayerPart(new PlayerError('no_player'), 'no_player')).toBe(true);
+  });
+
+  it('tells another error, or a problem that the player part does not show', () => {
+    expect(shownInPlayerPart(new PlayerError('no_player'), null)).toBe(false);
+    expect(shownInPlayerPart(new PlayerError('no_player'), undefined)).toBe(false);
+    expect(shownInPlayerPart(new PlayerError('no_overlay'), 'no_player')).toBe(false);
+    expect(shownInPlayerPart(new Error('no_player'), 'no_player')).toBe(false);
+  });
+});
+
 describe('playerCommandsOff', () => {
   it('turns off play and "audio to page" without notification access', () => {
     // The phone finding: with no notification access, play stayed on.