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); + } + }); + }); +}