From c1b149cc21e785f4f4e1ee3fe09b247a288ba5f7 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Mon, 18 May 2026 17:32:31 +0300 Subject: [PATCH] feat: implement real cross-platform voice recording utilizing record package with mic permission configuration --- .../android/app/src/main/AndroidManifest.xml | 6 + .../lib/controllers/chat_controller.dart | 85 ++++++++++ whatsapp_app/lib/screens/chat_screen.dart | 150 ++++++++++++------ .../flutter/generated_plugin_registrant.cc | 4 + .../linux/flutter/generated_plugins.cmake | 1 + .../Flutter/GeneratedPluginRegistrant.swift | 2 + whatsapp_app/pubspec.lock | 58 ++++++- whatsapp_app/pubspec.yaml | 2 + .../flutter/generated_plugin_registrant.cc | 3 + .../windows/flutter/generated_plugins.cmake | 1 + 10 files changed, 262 insertions(+), 50 deletions(-) diff --git a/whatsapp_app/android/app/src/main/AndroidManifest.xml b/whatsapp_app/android/app/src/main/AndroidManifest.xml index 2e5b67d..a23325d 100644 --- a/whatsapp_app/android/app/src/main/AndroidManifest.xml +++ b/whatsapp_app/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,10 @@ + + + + + + ().clearUnreadCount(conversation.id); } catch (_) {} + inputCtrl.addListener(() { + hasText.value = inputCtrl.text.trim().isNotEmpty; + }); + loadMessages(); markAsRead(); @@ -45,6 +60,8 @@ class ChatController extends GetxController { _svc.activeChatId.value = null; } _eventSub?.cancel(); + _recordTimer?.cancel(); + audioRecord.dispose(); inputCtrl.dispose(); scrollCtrl.dispose(); super.onClose(); @@ -228,4 +245,72 @@ class ChatController extends GetxController { return DateFormat('MMMM d, yyyy').format(dt); } } + + // ── Audio Recording Engine ─────────────────────────────────────────────── + Future startRecording() async { + try { + if (await audioRecord.hasPermission()) { + final tempDir = await getTemporaryDirectory(); + final path = '${tempDir.path}/rec_${DateTime.now().millisecondsSinceEpoch}.m4a'; + + await audioRecord.start( + const RecordConfig(encoder: AudioEncoder.aacLc), + path: path, + ); + + recordDuration.value = 0; + isRecording.value = true; + + _recordTimer?.cancel(); + _recordTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + recordDuration.value++; + }); + } else { + Get.snackbar( + 'Permission Denied', + 'Microphone permission is required to record voice notes.', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.redAccent.withOpacity(0.8), + colorText: Colors.white, + ); + } + } catch (e) { + print('[START RECORDING ERROR] $e'); + } + } + + Future stopAndSendRecording() async { + try { + _recordTimer?.cancel(); + final path = await audioRecord.stop(); + isRecording.value = false; + + if (path != null && recordDuration.value > 0) { + final file = File(path); + if (await file.exists()) { + final bytes = await file.readAsBytes(); + final base64String = base64Encode(bytes); + + await sendMediaMessage( + base64String, + 'audio/mp4', // Recorded as M4A (AAC), perfect for all platforms natively! + 'voice_note.m4a', + ); + } + } + } catch (e) { + print('[STOP RECORDING ERROR] $e'); + } + } + + Future cancelRecording() async { + try { + _recordTimer?.cancel(); + await audioRecord.stop(); + isRecording.value = false; + recordDuration.value = 0; + } catch (e) { + print('[CANCEL RECORDING ERROR] $e'); + } + } } diff --git a/whatsapp_app/lib/screens/chat_screen.dart b/whatsapp_app/lib/screens/chat_screen.dart index 1651e20..e08da71 100644 --- a/whatsapp_app/lib/screens/chat_screen.dart +++ b/whatsapp_app/lib/screens/chat_screen.dart @@ -139,62 +139,114 @@ class ChatScreen extends StatelessWidget { color: AppTheme.surface, padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), child: SafeArea( - child: Row( - children: [ - // Attachment button - IconButton( - icon: const Icon(Icons.add, color: AppTheme.primary, size: 28), - onPressed: () => _showAttachmentSheet(ctrl), - ), - // Input - Expanded( - child: TextField( - controller: ctrl.inputCtrl, - style: const TextStyle(color: AppTheme.textPrimary), - maxLines: 5, - minLines: 1, - textCapitalization: TextCapitalization.sentences, - decoration: InputDecoration( - hintText: 'Message', - hintStyle: const TextStyle(color: AppTheme.textSecondary), - filled: true, - fillColor: AppTheme.surfaceLight, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, vertical: 10, - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(24), - borderSide: BorderSide.none, + child: Obx(() { + if (ctrl.isRecording.value) { + return Row( + children: [ + const SizedBox(width: 12), + const Icon(Icons.fiber_manual_record, color: Colors.red, size: 16), + const SizedBox(width: 8), + const Text( + 'Recording...', + style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold, fontSize: 14), + ), + const SizedBox(width: 12), + Text( + '${(ctrl.recordDuration.value ~/ 60).toString().padLeft(2, '0')}:${(ctrl.recordDuration.value % 60).toString().padLeft(2, '0')}', + style: const TextStyle(color: AppTheme.textPrimary, fontSize: 14, fontFamily: 'monospace'), + ), + const Spacer(), + TextButton.icon( + icon: const Icon(Icons.delete, color: Colors.redAccent, size: 18), + label: const Text('Cancel', style: TextStyle(color: Colors.redAccent)), + onPressed: ctrl.cancelRecording, + ), + const SizedBox(width: 8), + GestureDetector( + onTap: ctrl.stopAndSendRecording, + child: Container( + width: 44, + height: 44, + decoration: const BoxDecoration( + color: AppTheme.primary, + shape: BoxShape.circle, + ), + child: const Icon(Icons.check, color: Colors.white, size: 20), ), ), - onSubmitted: (_) => ctrl.sendMessage(), + const SizedBox(width: 8), + ], + ); + } + + return Row( + children: [ + // Attachment button + IconButton( + icon: const Icon(Icons.add, color: AppTheme.primary, size: 28), + onPressed: () => _showAttachmentSheet(ctrl), ), - ), - const SizedBox(width: 8), - // Send button - Obx(() => GestureDetector( - onTap: ctrl.sendMessage, - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - width: 48, - height: 48, - decoration: const BoxDecoration( - color: AppTheme.primary, - shape: BoxShape.circle, + // Input + Expanded( + child: TextField( + controller: ctrl.inputCtrl, + style: const TextStyle(color: AppTheme.textPrimary), + maxLines: 5, + minLines: 1, + textCapitalization: TextCapitalization.sentences, + decoration: InputDecoration( + hintText: 'Message', + hintStyle: const TextStyle(color: AppTheme.textSecondary), + filled: true, + fillColor: AppTheme.surfaceLight, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 10, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(24), + borderSide: BorderSide.none, + ), + ), + onSubmitted: (_) => ctrl.sendMessage(), ), - child: ctrl.isSending.value - ? const Padding( - padding: EdgeInsets.all(12), - child: CircularProgressIndicator( - strokeWidth: 2, + ), + const SizedBox(width: 8), + // Dynamic Send / Mic Button + GestureDetector( + onTap: () { + if (ctrl.hasText.value) { + ctrl.sendMessage(); + } else { + ctrl.startRecording(); + } + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: 48, + height: 48, + decoration: const BoxDecoration( + color: AppTheme.primary, + shape: BoxShape.circle, + ), + child: ctrl.isSending.value + ? const Padding( + padding: EdgeInsets.all(12), + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : Icon( + ctrl.hasText.value ? Icons.send : Icons.mic, color: Colors.white, + size: 20, ), - ) - : const Icon(Icons.send, color: Colors.white, size: 20), + ), ), - )), - ], - ), + const SizedBox(width: 4), + ], + ); + }), ), ); diff --git a/whatsapp_app/linux/flutter/generated_plugin_registrant.cc b/whatsapp_app/linux/flutter/generated_plugin_registrant.cc index e0c16cd..64e0362 100644 --- a/whatsapp_app/linux/flutter/generated_plugin_registrant.cc +++ b/whatsapp_app/linux/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar = @@ -16,4 +17,7 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); + g_autoptr(FlPluginRegistrar) record_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "RecordLinuxPlugin"); + record_linux_plugin_register_with_registrar(record_linux_registrar); } diff --git a/whatsapp_app/linux/flutter/generated_plugins.cmake b/whatsapp_app/linux/flutter/generated_plugins.cmake index dd501cd..4ffc429 100644 --- a/whatsapp_app/linux/flutter/generated_plugins.cmake +++ b/whatsapp_app/linux/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST audioplayers_linux file_selector_linux + record_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/whatsapp_app/macos/Flutter/GeneratedPluginRegistrant.swift b/whatsapp_app/macos/Flutter/GeneratedPluginRegistrant.swift index 094b261..6bead75 100644 --- a/whatsapp_app/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/whatsapp_app/macos/Flutter/GeneratedPluginRegistrant.swift @@ -10,6 +10,7 @@ import file_selector_macos import firebase_core import firebase_messaging import flutter_local_notifications +import record_darwin import shared_preferences_foundation import sqflite_darwin @@ -19,6 +20,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) + RecordPlugin.register(with: registry.registrar(forPlugin: "RecordPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) } diff --git a/whatsapp_app/pubspec.lock b/whatsapp_app/pubspec.lock index c9ab778..ef8a785 100644 --- a/whatsapp_app/pubspec.lock +++ b/whatsapp_app/pubspec.lock @@ -601,7 +601,7 @@ packages: source: hosted version: "1.9.1" path_provider: - dependency: transitive + dependency: "direct main" description: name: path_provider sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" @@ -680,6 +680,46 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + record: + dependency: "direct main" + description: + name: record + sha256: "2e3d56d196abcd69f1046339b75e5f3855b2406fc087e5991f6703f188aa03a6" + url: "https://pub.dev" + source: hosted + version: "5.2.1" + record_android: + dependency: transitive + description: + name: record_android + sha256: "94783f08403aed33ffb68797bf0715b0812eb852f3c7985644c945faea462ba1" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + record_darwin: + dependency: transitive + description: + name: record_darwin + sha256: e487eccb19d82a9a39cd0126945cfc47b9986e0df211734e2788c95e3f63c82c + url: "https://pub.dev" + source: hosted + version: "1.2.2" + record_linux: + dependency: transitive + description: + name: record_linux + sha256: "74d41a9ebb1eb498a38e9a813dd524e8f0b4fdd627270bda9756f437b110a3e3" + url: "https://pub.dev" + source: hosted + version: "0.7.2" + record_platform_interface: + dependency: transitive + description: + name: record_platform_interface + sha256: "8a81dbc4e14e1272a285bbfef6c9136d070a47d9b0d1f40aa6193516253ee2f6" + url: "https://pub.dev" + source: hosted + version: "1.5.0" record_use: dependency: transitive description: @@ -688,6 +728,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.0" + record_web: + dependency: transitive + description: + name: record_web + sha256: a12856d0b3dd03d336b4b10d7520a8b3e21649a06a8f95815318feaa8f07adbb + url: "https://pub.dev" + source: hosted + version: "1.1.9" + record_windows: + dependency: transitive + description: + name: record_windows + sha256: "223258060a1d25c62bae18282c16783f28581ec19401d17e56b5205b9f039d78" + url: "https://pub.dev" + source: hosted + version: "1.0.7" rxdart: dependency: transitive description: diff --git a/whatsapp_app/pubspec.yaml b/whatsapp_app/pubspec.yaml index 48a0069..1c1645b 100644 --- a/whatsapp_app/pubspec.yaml +++ b/whatsapp_app/pubspec.yaml @@ -23,6 +23,8 @@ dependencies: flutter_contacts: ^1.1.7 image_picker: ^1.0.7 audioplayers: ^6.0.0 + record: ^5.1.2 + path_provider: ^2.1.2 dev_dependencies: flutter_test: diff --git a/whatsapp_app/windows/flutter/generated_plugin_registrant.cc b/whatsapp_app/windows/flutter/generated_plugin_registrant.cc index 9375ea8..67a27d3 100644 --- a/whatsapp_app/windows/flutter/generated_plugin_registrant.cc +++ b/whatsapp_app/windows/flutter/generated_plugin_registrant.cc @@ -9,6 +9,7 @@ #include #include #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { AudioplayersWindowsPluginRegisterWithRegistrar( @@ -17,4 +18,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("FileSelectorWindows")); FirebaseCorePluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); + RecordWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("RecordWindowsPluginCApi")); } diff --git a/whatsapp_app/windows/flutter/generated_plugins.cmake b/whatsapp_app/windows/flutter/generated_plugins.cmake index 24f787a..7d46645 100644 --- a/whatsapp_app/windows/flutter/generated_plugins.cmake +++ b/whatsapp_app/windows/flutter/generated_plugins.cmake @@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST audioplayers_windows file_selector_windows firebase_core + record_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST