diff --git a/whatsapp_app/ios/Runner/Info.plist b/whatsapp_app/ios/Runner/Info.plist
index 19b513a..93aa880 100644
--- a/whatsapp_app/ios/Runner/Info.plist
+++ b/whatsapp_app/ios/Runner/Info.plist
@@ -47,5 +47,11 @@
NSContactsUsageDescription
This app requires contacts access to match phone numbers with your local address book names.
+ NSCameraUsageDescription
+ This app requires camera access to take and send photos via WhatsApp.
+ NSPhotoLibraryUsageDescription
+ This app requires photo library access to choose and send photos via WhatsApp.
+ NSMicrophoneUsageDescription
+ This app requires microphone access to record and send audio messages via WhatsApp.
diff --git a/whatsapp_app/lib/screens/chat_screen.dart b/whatsapp_app/lib/screens/chat_screen.dart
index bb5901e..d42ab81 100644
--- a/whatsapp_app/lib/screens/chat_screen.dart
+++ b/whatsapp_app/lib/screens/chat_screen.dart
@@ -1,5 +1,7 @@
+import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
+import 'package:image_picker/image_picker.dart';
import '../controllers/chat_controller.dart';
import '../models/conversation_model.dart';
import '../models/message_model.dart';
@@ -232,19 +234,21 @@ class ChatScreen extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildAttachmentItem(
- icon: Icons.photo,
- color: Colors.purple,
- label: 'Photo',
+ icon: Icons.camera_alt,
+ color: Colors.green,
+ label: 'Camera',
onTap: () {
Get.back();
- // Real red dot 5x5 pixel PNG base64
- const base64Photo = 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==';
- ctrl.sendMediaMessage(
- base64Photo,
- 'image/png',
- 'photo.png',
- caption: '📸 Photo sent from Mywhatsapp App!',
- );
+ _pickAndSendImage(ctrl, ImageSource.camera);
+ },
+ ),
+ _buildAttachmentItem(
+ icon: Icons.photo_library,
+ color: Colors.purple,
+ label: 'Gallery',
+ onTap: () {
+ Get.back();
+ _pickAndSendImage(ctrl, ImageSource.gallery);
},
),
_buildAttachmentItem(
@@ -272,6 +276,41 @@ class ChatScreen extends StatelessWidget {
);
}
+ void _pickAndSendImage(ChatController ctrl, ImageSource source) async {
+ try {
+ final ImagePicker picker = ImagePicker();
+ final XFile? image = await picker.pickImage(
+ source: source,
+ imageQuality: 75,
+ );
+ if (image == null) return;
+
+ final bytes = await image.readAsBytes();
+ final base64String = base64Encode(bytes);
+
+ String mimetype = 'image/jpeg';
+ if (image.path.toLowerCase().endsWith('.png')) {
+ mimetype = 'image/png';
+ } else if (image.path.toLowerCase().endsWith('.gif')) {
+ mimetype = 'image/gif';
+ }
+
+ await ctrl.sendMediaMessage(
+ base64String,
+ mimetype,
+ image.name,
+ caption: '📸 Photo sent via Mywhatsapp!',
+ );
+ } catch (e) {
+ Get.snackbar(
+ 'Error picking image',
+ e.toString(),
+ backgroundColor: Colors.redAccent.withOpacity(0.8),
+ colorText: Colors.white,
+ );
+ }
+ }
+
Widget _buildAttachmentItem({
required IconData icon,
required Color color,
diff --git a/whatsapp_app/lib/widgets/message_bubble.dart b/whatsapp_app/lib/widgets/message_bubble.dart
index 174bab1..67e3c43 100644
--- a/whatsapp_app/lib/widgets/message_bubble.dart
+++ b/whatsapp_app/lib/widgets/message_bubble.dart
@@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:get/get.dart';
+import 'package:audioplayers/audioplayers.dart';
import '../models/message_model.dart';
import '../theme/app_theme.dart';
import '../services/whatsapp_service.dart';
@@ -146,46 +147,80 @@ class _InteractiveMediaWidgetState extends State {
final WhatsAppService _svc = Get.find();
bool _isLoading = false;
- // Audio simulation state
+ // Audio player state
+ final AudioPlayer _player = AudioPlayer();
+ StreamSubscription? _posSub;
+ StreamSubscription? _durSub;
+ StreamSubscription? _stateSub;
+
bool _isPlaying = false;
double _audioProgress = 0.0;
- int _audioDurationSeconds = 12;
+ int _audioDurationSeconds = 1;
int _audioCurrentSeconds = 0;
- Timer? _audioTimer;
@override
- void dispose() {
- _audioTimer?.cancel();
- super.dispose();
- }
-
- void _toggleAudioPlayback() {
- if (_isPlaying) {
- _audioTimer?.cancel();
- setState(() {
- _isPlaying = false;
- });
- } else {
- setState(() {
- _isPlaying = true;
- });
- const intervalMs = 100;
- _audioTimer = Timer.periodic(const Duration(milliseconds: intervalMs), (timer) {
- if (!mounted) {
- timer.cancel();
- return;
- }
+ void initState() {
+ super.initState();
+ _posSub = _player.onPositionChanged.listen((p) {
+ if (mounted) {
setState(() {
- _audioProgress += intervalMs / (_audioDurationSeconds * 1000);
- _audioCurrentSeconds = (_audioProgress * _audioDurationSeconds).floor();
- if (_audioProgress >= 1.0) {
+ _audioCurrentSeconds = p.inSeconds;
+ if (_audioDurationSeconds > 0) {
+ _audioProgress = p.inMilliseconds / (_audioDurationSeconds * 1000);
+ if (_audioProgress > 1.0) _audioProgress = 1.0;
+ }
+ });
+ }
+ });
+
+ _durSub = _player.onDurationChanged.listen((d) {
+ if (mounted) {
+ setState(() {
+ _audioDurationSeconds = d.inSeconds > 0 ? d.inSeconds : 1;
+ });
+ }
+ });
+
+ _stateSub = _player.onPlayerStateChanged.listen((s) {
+ if (mounted) {
+ setState(() {
+ _isPlaying = s == PlayerState.playing;
+ if (s == PlayerState.completed) {
_audioProgress = 0.0;
_audioCurrentSeconds = 0;
_isPlaying = false;
- timer.cancel();
}
});
- });
+ }
+ });
+ }
+
+ @override
+ void dispose() {
+ _posSub?.cancel();
+ _durSub?.cancel();
+ _stateSub?.cancel();
+ _player.dispose();
+ super.dispose();
+ }
+
+ void _toggleAudioPlayback(String base64Data) async {
+ try {
+ if (_isPlaying) {
+ await _player.pause();
+ } else {
+ final bytes = base64Decode(base64Data);
+ await _player.play(BytesSource(bytes));
+ }
+ } catch (e) {
+ print('[AUDIO PLAYBACK ERROR] $e');
+ Get.snackbar(
+ 'Playback Error',
+ 'Could not play audio message: $e',
+ snackPosition: SnackPosition.BOTTOM,
+ backgroundColor: Colors.redAccent.withOpacity(0.8),
+ colorText: Colors.white,
+ );
}
}
@@ -292,7 +327,7 @@ class _InteractiveMediaWidgetState extends State {
color: AppTheme.primary,
size: 24,
),
- onPressed: _toggleAudioPlayback,
+ onPressed: () => _toggleAudioPlayback(base64Data),
),
Expanded(
child: Padding(
diff --git a/whatsapp_app/linux/flutter/generated_plugin_registrant.cc b/whatsapp_app/linux/flutter/generated_plugin_registrant.cc
index e71a16d..e0c16cd 100644
--- a/whatsapp_app/linux/flutter/generated_plugin_registrant.cc
+++ b/whatsapp_app/linux/flutter/generated_plugin_registrant.cc
@@ -6,6 +6,14 @@
#include "generated_plugin_registrant.h"
+#include
+#include
void fl_register_plugins(FlPluginRegistry* registry) {
+ g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar =
+ fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin");
+ audioplayers_linux_plugin_register_with_registrar(audioplayers_linux_registrar);
+ 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);
}
diff --git a/whatsapp_app/linux/flutter/generated_plugins.cmake b/whatsapp_app/linux/flutter/generated_plugins.cmake
index be1ee3e..dd501cd 100644
--- a/whatsapp_app/linux/flutter/generated_plugins.cmake
+++ b/whatsapp_app/linux/flutter/generated_plugins.cmake
@@ -3,6 +3,8 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
+ audioplayers_linux
+ file_selector_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
diff --git a/whatsapp_app/macos/Flutter/GeneratedPluginRegistrant.swift b/whatsapp_app/macos/Flutter/GeneratedPluginRegistrant.swift
index 93cf9f5..094b261 100644
--- a/whatsapp_app/macos/Flutter/GeneratedPluginRegistrant.swift
+++ b/whatsapp_app/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -5,6 +5,8 @@
import FlutterMacOS
import Foundation
+import audioplayers_darwin
+import file_selector_macos
import firebase_core
import firebase_messaging
import flutter_local_notifications
@@ -12,6 +14,8 @@ import shared_preferences_foundation
import sqflite_darwin
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
+ AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
+ FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
diff --git a/whatsapp_app/pubspec.lock b/whatsapp_app/pubspec.lock
index 4b47030..c9ab778 100644
--- a/whatsapp_app/pubspec.lock
+++ b/whatsapp_app/pubspec.lock
@@ -25,6 +25,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.13.1"
+ audioplayers:
+ dependency: "direct main"
+ description:
+ name: audioplayers
+ sha256: a72dd459d1a48f61a6fb9c0134dba26597c9236af40639ff0eb70eb4e0baab70
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.6.0"
+ audioplayers_android:
+ dependency: transitive
+ description:
+ name: audioplayers_android
+ sha256: "60a6728277228413a85755bd3ffd6fab98f6555608923813ce383b190a360605"
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.2.1"
+ audioplayers_darwin:
+ dependency: transitive
+ description:
+ name: audioplayers_darwin
+ sha256: c994b3bb3a921e4904ac40e013fbc94488e824fd7c1de6326f549943b0b44a91
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.4.0"
+ audioplayers_linux:
+ dependency: transitive
+ description:
+ name: audioplayers_linux
+ sha256: f75bce1ce864170ef5e6a2c6a61cd3339e1a17ce11e99a25bae4474ea491d001
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.2.1"
+ audioplayers_platform_interface:
+ dependency: transitive
+ description:
+ name: audioplayers_platform_interface
+ sha256: "0e2f6a919ab56d0fec272e801abc07b26ae7f31980f912f24af4748763e5a656"
+ url: "https://pub.dev"
+ source: hosted
+ version: "7.1.1"
+ audioplayers_web:
+ dependency: transitive
+ description:
+ name: audioplayers_web
+ sha256: faa8fa6587f996a6f604433b53af44c57a1407d4fe8dff5766cf63d6875e8de9
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.2.0"
+ audioplayers_windows:
+ dependency: transitive
+ description:
+ name: audioplayers_windows
+ sha256: bafff2b38b6f6d331887558ba6e0a01c9c208d9dbb3ad0005234db065122a734
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.3.0"
boolean_selector:
dependency: transitive
description:
@@ -89,6 +145,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.19.1"
+ cross_file:
+ dependency: transitive
+ description:
+ name: cross_file
+ sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.3.5+2"
crypto:
dependency: transitive
description:
@@ -137,6 +201,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.1"
+ file_selector_linux:
+ dependency: transitive
+ description:
+ name: file_selector_linux
+ sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.9.4"
+ file_selector_macos:
+ dependency: transitive
+ description:
+ name: file_selector_macos
+ sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.9.5"
+ file_selector_platform_interface:
+ dependency: transitive
+ description:
+ name: file_selector_platform_interface
+ sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.7.0"
+ file_selector_windows:
+ dependency: transitive
+ description:
+ name: file_selector_windows
+ sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.9.3+5"
firebase_core:
dependency: "direct main"
description:
@@ -246,6 +342,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.2.0"
+ flutter_plugin_android_lifecycle:
+ dependency: transitive
+ description:
+ name: flutter_plugin_android_lifecycle
+ sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.34"
flutter_test:
dependency: "direct dev"
description: flutter
@@ -296,6 +400,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.2"
+ image_picker:
+ dependency: "direct main"
+ description:
+ name: image_picker
+ sha256: "91c025426c2881c551100bce834e201c835a170151545f58d17da5180ca7d9ac"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.2.2"
+ image_picker_android:
+ dependency: transitive
+ description:
+ name: image_picker_android
+ sha256: d5b3e1774af29c9ab00103afb0d4614070f924d2e0057ac867ec98800114793f
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.8.13+17"
+ image_picker_for_web:
+ dependency: transitive
+ description:
+ name: image_picker_for_web
+ sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.1"
+ image_picker_ios:
+ dependency: transitive
+ description:
+ name: image_picker_ios
+ sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.8.13+6"
+ image_picker_linux:
+ dependency: transitive
+ description:
+ name: image_picker_linux
+ sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.2"
+ image_picker_macos:
+ dependency: transitive
+ description:
+ name: image_picker_macos
+ sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.2+1"
+ image_picker_platform_interface:
+ dependency: transitive
+ description:
+ name: image_picker_platform_interface
+ sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.11.1"
+ image_picker_windows:
+ dependency: transitive
+ description:
+ name: image_picker_windows
+ sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.2"
intl:
dependency: "direct main"
description:
@@ -384,6 +552,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.17.0"
+ mime:
+ dependency: transitive
+ description:
+ name: mime
+ sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.0"
native_toolchain_c:
dependency: transitive
description:
diff --git a/whatsapp_app/pubspec.yaml b/whatsapp_app/pubspec.yaml
index bb0c84f..48a0069 100644
--- a/whatsapp_app/pubspec.yaml
+++ b/whatsapp_app/pubspec.yaml
@@ -21,6 +21,8 @@ dependencies:
firebase_messaging: ^14.9.1
flutter_local_notifications: ^17.1.2
flutter_contacts: ^1.1.7
+ image_picker: ^1.0.7
+ audioplayers: ^6.0.0
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 1a82e7d..9375ea8 100644
--- a/whatsapp_app/windows/flutter/generated_plugin_registrant.cc
+++ b/whatsapp_app/windows/flutter/generated_plugin_registrant.cc
@@ -6,9 +6,15 @@
#include "generated_plugin_registrant.h"
+#include
+#include
#include
void RegisterPlugins(flutter::PluginRegistry* registry) {
+ AudioplayersWindowsPluginRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin"));
+ FileSelectorWindowsRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("FileSelectorWindows"));
FirebaseCorePluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FirebaseCorePluginCApi"));
}
diff --git a/whatsapp_app/windows/flutter/generated_plugins.cmake b/whatsapp_app/windows/flutter/generated_plugins.cmake
index b854f96..24f787a 100644
--- a/whatsapp_app/windows/flutter/generated_plugins.cmake
+++ b/whatsapp_app/windows/flutter/generated_plugins.cmake
@@ -3,6 +3,8 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
+ audioplayers_windows
+ file_selector_windows
firebase_core
)
diff --git a/whatsapp_bridge/server.js b/whatsapp_bridge/server.js
index 041dc21..9684efb 100644
--- a/whatsapp_bridge/server.js
+++ b/whatsapp_bridge/server.js
@@ -409,6 +409,19 @@ async function handleMessage(ws, raw) {
return respond({ type: 'error', message: 'Failed to download media file from WhatsApp servers after multiple attempts' });
}
+ // If the media is an Ogg/Opus audio file, convert it to MP3 on-the-fly
+ if (media.mimetype && (media.mimetype.includes('audio/ogg') || media.mimetype.includes('ogg'))) {
+ try {
+ console.log(`[WS] Converting OGG audio file for message ${messageId} to MP3 for iOS compatibility...`);
+ const mp3Data = await convertOggToMp3(media.data);
+ media.data = mp3Data;
+ media.mimetype = 'audio/mp3';
+ media.filename = 'voice_note.mp3';
+ } catch (err) {
+ console.error(`[WS] Ogg to MP3 conversion failed (sending raw Ogg instead):`, err.message);
+ }
+ }
+
return respond({
type: 'media',
messageId: messageId,
@@ -561,3 +574,37 @@ server.listen(PORT, () => {
console.log(`[SERVER] Standalone WhatsApp Bridge running on port ${PORT}`);
initWhatsApp();
});
+
+// ─── OGG to MP3 base64 converter using ffmpeg child process ────────────────
+function convertOggToMp3(base64Ogg) {
+ const { exec } = require('child_process');
+ const tmp = require('os').tmpdir();
+ const path = require('path');
+ const fs = require('fs');
+
+ return new Promise((resolve, reject) => {
+ const timeId = Date.now();
+ const inputPath = path.join(tmp, `input_${timeId}.ogg`);
+ const outputPath = path.join(tmp, `output_${timeId}.mp3`);
+
+ fs.writeFileSync(inputPath, Buffer.from(base64Ogg, 'base64'));
+
+ exec(`ffmpeg -i "${inputPath}" -acodec libmp3lame -aq 2 "${outputPath}"`, (error, stdout, stderr) => {
+ // Clean up input file
+ try { fs.unlinkSync(inputPath); } catch(_) {}
+
+ if (error) {
+ console.error('[FFMPEG ERROR]', error);
+ return reject(error);
+ }
+
+ try {
+ const mp3Base64 = fs.readFileSync(outputPath).toString('base64');
+ try { fs.unlinkSync(outputPath); } catch(_) {}
+ resolve(mp3Base64);
+ } catch (err) {
+ reject(err);
+ }
+ });
+ });
+}