From af1543135d42adc2e97dc5243aeef7418cd3b00d Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Sat, 07 Mar 2026 08:39:26 +0100
Subject: [PATCH] feat: dual address auto-switch, custom icon, notifications, image support

---
 services/audio.ts |  111 +++++++++++++++++++++++++++++++++++++++++++------------
 1 files changed, 87 insertions(+), 24 deletions(-)

diff --git a/services/audio.ts b/services/audio.ts
index 769d299..5fa8bd5 100644
--- a/services/audio.ts
+++ b/services/audio.ts
@@ -3,6 +3,7 @@
   requestRecordingPermissionsAsync,
   setAudioModeAsync,
 } from "expo-audio";
+import * as LegacyFileSystem from "expo-file-system/legacy";
 
 export interface RecordingResult {
   uri: string;
@@ -10,41 +11,103 @@
 }
 
 let currentPlayer: ReturnType<typeof createAudioPlayer> | null = null;
+const playingListeners = new Set<(playing: boolean) => void>();
+
+// Audio queue for chaining sequential voice notes
+const audioQueue: Array<{ uri: string; onFinish?: () => void }> = [];
+let processingQueue = false;
+
+function notifyListeners(playing: boolean): void {
+  for (const cb of playingListeners) cb(playing);
+}
+
+export function onPlayingChange(cb: (playing: boolean) => void): () => void {
+  playingListeners.add(cb);
+  return () => { playingListeners.delete(cb); };
+}
 
 export async function requestPermissions(): Promise<boolean> {
   const { status } = await requestRecordingPermissionsAsync();
   return status === "granted";
 }
 
+let audioCounter = 0;
+
+/**
+ * Convert a base64 audio string to a file URI.
+ */
+export async function saveBase64Audio(base64: string, ext = "m4a"): Promise<string> {
+  const tmpPath = `${LegacyFileSystem.cacheDirectory}pailot-voice-${++audioCounter}.${ext}`;
+  await LegacyFileSystem.writeAsStringAsync(tmpPath, base64, {
+    encoding: LegacyFileSystem.EncodingType.Base64,
+  });
+  return tmpPath;
+}
+
+/**
+ * Queue audio for playback. Multiple calls chain sequentially —
+ * the next voice note plays only after the current one finishes.
+ */
 export async function playAudio(
   uri: string,
   onFinish?: () => void
 ): Promise<void> {
-  try {
-    await stopPlayback();
-
-    await setAudioModeAsync({
-      playsInSilentMode: true,
-    });
-
-    const player = createAudioPlayer(uri);
-    currentPlayer = player;
-
-    player.addListener("playbackStatusUpdate", (status) => {
-      if (!status.playing && status.currentTime >= status.duration && status.duration > 0) {
-        onFinish?.();
-        player.remove();
-        if (currentPlayer === player) currentPlayer = null;
-      }
-    });
-
-    player.play();
-  } catch (error) {
-    console.error("Failed to play audio:", error);
+  audioQueue.push({ uri, onFinish });
+  if (!processingQueue) {
+    processAudioQueue();
   }
 }
 
+async function processAudioQueue(): Promise<void> {
+  if (processingQueue) return;
+  processingQueue = true;
+
+  while (audioQueue.length > 0) {
+    const item = audioQueue.shift()!;
+    await playOneAudio(item.uri, item.onFinish);
+  }
+
+  processingQueue = false;
+}
+
+function playOneAudio(uri: string, onFinish?: () => void): Promise<void> {
+  return new Promise<void>(async (resolve) => {
+    try {
+      await setAudioModeAsync({ playsInSilentMode: true });
+
+      const player = createAudioPlayer(uri);
+      currentPlayer = player;
+      notifyListeners(true);
+
+      player.addListener("playbackStatusUpdate", (status) => {
+        if (!status.playing && status.currentTime >= status.duration && status.duration > 0) {
+          onFinish?.();
+          player.remove();
+          if (currentPlayer === player) {
+            currentPlayer = null;
+            if (audioQueue.length === 0) notifyListeners(false);
+          }
+          resolve();
+        }
+      });
+
+      player.play();
+    } catch (error) {
+      console.error("Failed to play audio:", error);
+      resolve();
+    }
+  });
+}
+
+export function isPlaying(): boolean {
+  return currentPlayer !== null;
+}
+
+/**
+ * Stop current playback and clear the queue.
+ */
 export async function stopPlayback(): Promise<void> {
+  audioQueue.length = 0;
   if (currentPlayer) {
     try {
       currentPlayer.pause();
@@ -53,13 +116,13 @@
       // Ignore cleanup errors
     }
     currentPlayer = null;
+    notifyListeners(false);
   }
 }
 
 export async function encodeAudioToBase64(uri: string): Promise<string> {
-  const FileSystem = await import("expo-file-system");
-  const result = await FileSystem.readAsStringAsync(uri, {
-    encoding: FileSystem.EncodingType.Base64,
+  const result = await LegacyFileSystem.readAsStringAsync(uri, {
+    encoding: LegacyFileSystem.EncodingType.Base64,
   });
   return result;
 }

--
Gitblit v1.3.1