From d6cf9469aa0462d1b8313cc85907176eee1214a2 Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Wed, 25 Mar 2026 17:10:54 +0100
Subject: [PATCH] fix: C3 debug logs, H1-H5 image cache, temp files, controller leak, validation, lifecycle

---
 lib/services/audio_service.dart |   25 ++++++++++++++++++++++++-
 1 files changed, 24 insertions(+), 1 deletions(-)

diff --git a/lib/services/audio_service.dart b/lib/services/audio_service.dart
index 76843e0..a547e6f 100644
--- a/lib/services/audio_service.dart
+++ b/lib/services/audio_service.dart
@@ -28,9 +28,16 @@
   // Autoplay suppression
   static bool _isBackgrounded = false;
 
+  // Track last played temp file so it can be cleaned up when the track ends
+  static String? _lastPlaybackTempPath;
+
+  // Lifecycle observer stored so we can remove it in dispose()
+  static _LifecycleObserver? _lifecycleObserver;
+
   /// Initialize the audio service and set up lifecycle observer.
   static void init() {
-    WidgetsBinding.instance.addObserver(_LifecycleObserver());
+    _lifecycleObserver = _LifecycleObserver();
+    WidgetsBinding.instance.addObserver(_lifecycleObserver!);
 
     // Configure audio session for background playback
     _player.setAudioContext(AudioContext(
@@ -52,6 +59,13 @@
   }
 
   static void _onTrackComplete() {
+    // Clean up the temp file that just finished playing
+    final prev = _lastPlaybackTempPath;
+    _lastPlaybackTempPath = null;
+    if (prev != null) {
+      File(prev).delete().ignore();
+    }
+
     if (_queue.isNotEmpty) {
       _playNextInQueue();
     } else {
@@ -68,6 +82,7 @@
     }
 
     final path = _queue.removeAt(0);
+    _lastPlaybackTempPath = path;
     try {
       // Brief pause between tracks — iOS audio player needs time to reset
       await _player.stop();
@@ -143,10 +158,12 @@
 
     if (source.startsWith('/')) {
       await _player.play(DeviceFileSource(source));
+      // File path owned by caller — not tracked for deletion
     } else {
       // base64 data — write to temp file first
       final path = await _base64ToFile(source);
       if (path == null) return;
+      _lastPlaybackTempPath = path;
       await _player.play(DeviceFileSource(path));
     }
     _isPlaying = true;
@@ -159,6 +176,7 @@
     final path = await _base64ToFile(base64Audio);
     if (path == null) return;
 
+    _lastPlaybackTempPath = path;
     await _player.play(DeviceFileSource(path));
     _isPlaying = true;
     onPlaybackStateChanged?.call();
@@ -177,6 +195,7 @@
       debugPrint('AudioService: queued (queue size: ${_queue.length})');
     } else {
       // Nothing playing — start immediately
+      _lastPlaybackTempPath = path;
       try {
         await _player.play(DeviceFileSource(path));
         _isPlaying = true;
@@ -250,6 +269,10 @@
   }
 
   static Future<void> dispose() async {
+    if (_lifecycleObserver != null) {
+      WidgetsBinding.instance.removeObserver(_lifecycleObserver!);
+      _lifecycleObserver = null;
+    }
     await cancelRecording();
     await stopPlayback();
     _recorder.dispose();

--
Gitblit v1.3.1