From 1348a30ecab1ea19eb6e62d8ea67c5a82822c148 Mon Sep 17 00:00:00 2001 From: dikshantbhangala Date: Mon, 30 Dec 2024 15:28:07 +0530 Subject: [PATCH 1/5] done with change --- lib/services/communication_service.dart | 0 macos/Flutter/GeneratedPluginRegistrant.swift | 2 +- .../MainActivity.kt | 0 pubspec.yaml | 1 + 4 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 lib/services/communication_service.dart rename {android/app/src/main/kotlin/com/nankai/openpeerchat_flutter => openpeerchat_flutter}/MainActivity.kt (100%) diff --git a/lib/services/communication_service.dart b/lib/services/communication_service.dart new file mode 100644 index 0000000..e69de29 diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 14cd431..9d4b458 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,7 +9,7 @@ import flutter_secure_storage_macos import local_auth_darwin import path_provider_foundation import shared_preferences_foundation -import sqflite +import sqflite_darwin func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) diff --git a/android/app/src/main/kotlin/com/nankai/openpeerchat_flutter/MainActivity.kt b/openpeerchat_flutter/MainActivity.kt similarity index 100% rename from android/app/src/main/kotlin/com/nankai/openpeerchat_flutter/MainActivity.kt rename to openpeerchat_flutter/MainActivity.kt diff --git a/pubspec.yaml b/pubspec.yaml index b3040f7..70a4a2f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -32,6 +32,7 @@ dependencies: open_filex: ^4.5.0 permission_handler: ^11.3.1 path_provider: ^2.1.4 + web_socket_channel: ^3.0.1 dev_dependencies: flutter_lints: From 1ed7a2a3ab247769f78a0cd7c1577c043454e36c Mon Sep 17 00:00:00 2001 From: dikshantbhangala Date: Mon, 30 Dec 2024 15:44:53 +0530 Subject: [PATCH 2/5] done --- lib/classes/global.dart | 2 + lib/pages/chat_page.dart | 147 +++++++++++++----------- lib/pages/profile.dart | 4 +- lib/services/communication_service.dart | 32 ++++++ openpeerchat_flutter/MainActivity.kt | 7 -- 5 files changed, 115 insertions(+), 77 deletions(-) delete mode 100644 openpeerchat_flutter/MainActivity.kt diff --git a/lib/classes/global.dart b/lib/classes/global.dart index 6af0a7b..fb4d39f 100644 --- a/lib/classes/global.dart +++ b/lib/classes/global.dart @@ -28,6 +28,8 @@ class Global extends ChangeNotifier { static Map cache = {}; static final GlobalKey scaffoldKey = GlobalKey(); + static var profileNameStream; + void sentToConversations(Msg msg, String converser, {bool addToTable = true}) { diff --git a/lib/pages/chat_page.dart b/lib/pages/chat_page.dart index 33a2518..d2883ad 100644 --- a/lib/pages/chat_page.dart +++ b/lib/pages/chat_page.dart @@ -13,26 +13,31 @@ import '../components/view_file.dart'; import '../encyption/rsa.dart'; class ChatPage extends StatefulWidget { - const ChatPage({Key? key, required this.converser}) : super(key: key); + String converser; + ChatPage({Key? key, required this.converser}) : super(key: key); + - final String converser; @override - ChatPageState createState() => ChatPageState(); + _ChatPageState createState() => _ChatPageState(); } -class ChatPageState extends State { +class _ChatPageState extends State { List messageList = []; TextEditingController myController = TextEditingController(); @override void initState() { super.initState(); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); + // Adding listener to listen for profile name updates + Global.profileNameStream.listen((updatedName) { + setState(() { + if (widget.converser == Global.myName) { + // Update the converser name if it matches the updated profile name + widget.converser = updatedName; + } + }); + }); } final ScrollController _scrollController = ScrollController(); @@ -74,72 +79,78 @@ class ChatPageState extends State { Expanded( child: messageList.isEmpty ? const Center( - child: Text('No messages yet'), - ) + child: Text('No messages yet'), + ) : ListView.builder( - controller: _scrollController, - padding: const EdgeInsets.all(8), - itemCount: groupedMessages.keys.length, - itemBuilder: (BuildContext context, int index) { - String date = groupedMessages.keys.elementAt(index); - return Column( - children: [ - Center( - child: Padding( - padding: const EdgeInsets.only(top: 10), - child: Text( - date, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - ), - ), - ...groupedMessages[date]!.map((msg) { - String displayMessage = msg.message; - if (Global.myPrivateKey != null) { - RSAPrivateKey privateKey = Global.myPrivateKey!; - dynamic data = jsonDecode(msg.message); - if (data['type'] == 'text') { - Uint8List encryptedBytes = base64Decode(data['data']); - Uint8List decryptedBytes = rsaDecrypt(privateKey, encryptedBytes); - displayMessage = utf8.decode(decryptedBytes); - } - } + controller: _scrollController, + padding: const EdgeInsets.all(8), + itemCount: groupedMessages.keys.length, + itemBuilder: (BuildContext context, int index) { + String date = groupedMessages.keys.elementAt(index); return Column( - crossAxisAlignment: msg.msgtype == 'sent' ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ - Align( - alignment: msg.msgtype == 'sent' ? Alignment.centerRight : Alignment.centerLeft, - child: Bubble( - padding: const BubbleEdges.all(12), - margin: const BubbleEdges.only(top: 10), - //add shadow - style: BubbleStyle( - elevation: 3, - shadowColor: Colors.black.withOpacity(0.5), - ), - // nip: msg.msgtype == 'sent' ? BubbleNip.rightTop : BubbleNip.leftTop, - radius: const Radius.circular(10), - color: msg.msgtype == 'sent' ? const Color(0xffd1c4e9) : const Color(0xff80DEEA), - child: msg.message.contains('file') ? _buildFileBubble(msg) : Text( - displayMessage, - style: const TextStyle(color: Colors.black87), + Center( + child: Padding( + padding: const EdgeInsets.only(top: 10), + child: Text( + date, + style: const TextStyle(fontWeight: FontWeight.bold), ), ), ), - Padding( - padding: const EdgeInsets.only(top: 2, bottom: 10), - child: Text( - dateFormatter(timeStamp: msg.timestamp), - style: const TextStyle(color: Colors.black54, fontSize: 10), - ), - ), + ...groupedMessages[date]!.map((msg) { + String displayMessage = msg.message; + if (Global.myPrivateKey != null) { + RSAPrivateKey privateKey = Global.myPrivateKey!; + dynamic data = jsonDecode(msg.message); + if (data['type'] == 'text') { + Uint8List encryptedBytes = base64Decode(data['data']); + Uint8List decryptedBytes = rsaDecrypt(privateKey, encryptedBytes); + displayMessage = utf8.decode(decryptedBytes); + } + } + return Column( + crossAxisAlignment: msg.msgtype == 'sent' + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + children: [ + Align( + alignment: msg.msgtype == 'sent' + ? Alignment.centerRight + : Alignment.centerLeft, + child: Bubble( + padding: const BubbleEdges.all(12), + margin: const BubbleEdges.only(top: 10), + style: BubbleStyle( + elevation: 3, + shadowColor: Colors.black.withOpacity(0.5), + ), + radius: const Radius.circular(10), + color: msg.msgtype == 'sent' + ? const Color(0xffd1c4e9) + : const Color(0xff80DEEA), + child: msg.message.contains('file') + ? _buildFileBubble(msg) + : Text( + displayMessage, + style: const TextStyle(color: Colors.black87), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 2, bottom: 10), + child: Text( + dateFormatter(timeStamp: msg.timestamp), + style: const TextStyle(color: Colors.black54, fontSize: 10), + ), + ), + ], + ); + }), ], ); - }), - ], - ); - }, - ), + }, + ), ), MessagePanel(converser: widget.converser), ], @@ -161,7 +172,6 @@ class ChatPageState extends State { color: Colors.black87, ), overflow: TextOverflow.visible, - ), ), IconButton( @@ -180,3 +190,4 @@ String dateFormatter({required String timeStamp}) { String formattedTime = DateFormat('hh:mm aa').format(dateTime); return formattedTime; } + diff --git a/lib/pages/profile.dart b/lib/pages/profile.dart index 6e344e7..1dac65c 100644 --- a/lib/pages/profile.dart +++ b/lib/pages/profile.dart @@ -4,7 +4,7 @@ import 'home_screen.dart'; import 'package:nanoid/nanoid.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../classes/global.dart'; - +import '../services/communication_service.dart'; class Profile extends StatefulWidget { final bool onLogin; @@ -113,7 +113,7 @@ class _ProfileState extends State { // saving the name and id to shared preferences prefs.setString('p_name', myName.text); prefs.setString('p_id', customLengthId); - + CommunicationService.broadcastProfileUpdate(customLengthId, myName.text); // On pressing, move to the home screen navigateToHomeScreen(); }, diff --git a/lib/services/communication_service.dart b/lib/services/communication_service.dart index e69de29..3681a17 100644 --- a/lib/services/communication_service.dart +++ b/lib/services/communication_service.dart @@ -0,0 +1,32 @@ +import 'dart:convert'; +// ignore: depend_on_referenced_packages +import 'package:web_socket_channel/web_socket_channel.dart'; + +class CommunicationService { + static final WebSocketChannel _channel = + WebSocketChannel.connect(Uri.parse('ws://your-websocket-server-url')); + + /// Broadcasts a profile update to all connected peers + static void broadcastProfileUpdate(String userId, String newName) { + final message = { + 'type': 'profile_update', + 'userId': userId, + 'newName': newName, + }; + + _channel.sink.add(jsonEncode(message)); + } + + /// Listens for incoming messages + static void listen(void Function(Map) onMessage) { + _channel.stream.listen((data) { + final decodedData = jsonDecode(data); + onMessage(decodedData); + }); + } + + /// Closes the WebSocket connection + static void closeConnection() { + _channel.sink.close(); + } +} diff --git a/openpeerchat_flutter/MainActivity.kt b/openpeerchat_flutter/MainActivity.kt deleted file mode 100644 index d9d9e99..0000000 --- a/openpeerchat_flutter/MainActivity.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.nankai.openpeerchat_flutter - -import io.flutter.embedding.android.FlutterFragmentActivity - -class MainActivity: FlutterFragmentActivity() { - // ... -} \ No newline at end of file From c669a236a9c69f22165bf8f66a6ae6d4bb9aec77 Mon Sep 17 00:00:00 2001 From: dikshantbhangala Date: Fri, 3 Jan 2025 17:15:20 +0530 Subject: [PATCH 3/5] Added audio recording feature and refactored code --- lib/components/message_panel.dart | 135 +++++++++++++----- lib/encyption/rsa.dart | 69 ++++----- lib/pages/chat_page.dart | 227 +++++++++++++++--------------- pubspec.yaml | 1 + 4 files changed, 250 insertions(+), 182 deletions(-) diff --git a/lib/components/message_panel.dart b/lib/components/message_panel.dart index c5db219..637efbc 100644 --- a/lib/components/message_panel.dart +++ b/lib/components/message_panel.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_sound/flutter_sound.dart'; // ADDED import 'package:flutter/services.dart'; import 'package:nanoid/nanoid.dart'; import 'package:pointycastle/asymmetric/api.dart'; @@ -13,10 +14,6 @@ import '../database/database_helper.dart'; import '../encyption/rsa.dart'; import 'view_file.dart'; -/// This component is used in the ChatPage. -/// It is the message bar where the message is typed on and sent to -/// connected devices. - class MessagePanel extends StatefulWidget { const MessagePanel({Key? key, required this.converser}) : super(key: key); final String converser; @@ -28,19 +25,41 @@ class MessagePanel extends StatefulWidget { class _MessagePanelState extends State { TextEditingController myController = TextEditingController(); File _selectedFile = File(''); + FlutterSoundRecorder? _recorder; // ADDED + bool _isRecording = false; // ADDED + String? _recordedFilePath; // ADDED + + @override + void initState() { + super.initState(); + _recorder = FlutterSoundRecorder(); // ADDED + _initializeRecorder(); // ADDED + } + + @override + void dispose() { + _recorder?.closeRecorder(); // ADDED + _recorder = null; // ADDED + super.dispose(); + } + + // ADDED: Initialize audio recorder + Future _initializeRecorder() async { + await _recorder?.openRecorder(); + await _recorder?.setSubscriptionDuration(const Duration(milliseconds: 100)); + } @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(8.0), child: TextFormField( - //multiline text field maxLines: null, controller: myController, decoration: InputDecoration( icon: const Icon(Icons.person), hintText: 'Send Message?', - labelText: 'Send Message ', + labelText: 'Send Message', suffixIcon: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisSize: MainAxisSize.min, @@ -49,6 +68,13 @@ class _MessagePanelState extends State { onPressed: () => _navigateToFilePreviewPage(context), icon: const Icon(Icons.attach_file), ), + IconButton( + onPressed: _toggleRecording, // ADDED + icon: Icon( + _isRecording ? Icons.mic_off : Icons.mic, // ADDED + color: _isRecording ? Colors.red : null, // ADDED + ), + ), IconButton( onPressed: () => _sendMessage(context), icon: const Icon( @@ -67,7 +93,6 @@ class _MessagePanelState extends State { if (myController.text.isEmpty) { return; } - // Encode the message to base64 String data = jsonEncode({ "sender": Global.myName, @@ -95,7 +120,6 @@ class _MessagePanelState extends State { ); RSAPublicKey publicKey = Global.myPublicKey!; - // Encrypt the message Uint8List encryptedMessage = rsaEncrypt( publicKey, Uint8List.fromList(utf8.encode(myController.text))); @@ -110,21 +134,76 @@ class _MessagePanelState extends State { widget.converser, ); - // refreshMessages(); myController.clear(); } - /// This function is used to navigate to the file preview page and check the file size. + // ADDED: Start and stop audio recording + Future _toggleRecording() async { + if (_isRecording) { + final path = await _recorder?.stopRecorder(); + setState(() { + _isRecording = false; + _recordedFilePath = path; + }); + if (path != null) { + _sendAudioMessage(context, path); // Send the recorded audio + } + } else { + await _recorder?.startRecorder( + codec: Codec.aacMP4, + toFile: 'audio_${DateTime.now().millisecondsSinceEpoch}.m4a', + ); + setState(() { + _isRecording = true; + }); + } + } + + // ADDED: Send recorded audio as a message + void _sendAudioMessage(BuildContext context, String filePath) { + var msgId = nanoid(21); + String fileName = filePath.split('/').last; + + String data = jsonEncode({ + "sender": Global.myName, + "type": "audio", + "fileName": fileName, + "filePath": filePath, + }); + + String date = DateTime.now().toUtc().toString(); + Global.cache[msgId] = Payload( + msgId, + Global.myName, + widget.converser, + data, + date, + ); + insertIntoMessageTable( + Payload( + msgId, + Global.myName, + widget.converser, + data, + date, + ), + ); + + Provider.of(context, listen: false).sentToConversations( + Msg(data, "sent", date, msgId), + widget.converser, + ); + } + + // Existing file picker and sender logic void _navigateToFilePreviewPage(BuildContext context) async { - //max size of file is 30 MB double sizeKbs = 0; const int maxSizeKbs = 30 * 1024; FilePickerResult? result = await FilePicker.platform.pickFiles(); - if(result != null) { + if (result != null) { sizeKbs = result.files.single.size / 1024; } - if (sizeKbs > maxSizeKbs) { if (!context.mounted) return; showDialog( @@ -136,10 +215,8 @@ class _MessagePanelState extends State { mainAxisSize: MainAxisSize.min, children: [ ListTile( - //file size in MB title: Text('File Size: ${(sizeKbs / 1024).ceil()} MB'), - subtitle: const Text( - 'File size should not exceed 30 MB'), + subtitle: const Text('File size should not exceed 30 MB'), ), ], ), @@ -157,7 +234,6 @@ class _MessagePanelState extends State { return; } -//this function is used to open the file preview dialog if (result != null) { setState(() { _selectedFile = File(result.files.single.path!); @@ -172,12 +248,11 @@ class _MessagePanelState extends State { mainAxisSize: MainAxisSize.min, children: [ ListTile( - - title: Text('File Name: ${_selectedFile.path - .split('/') - .last}', overflow: TextOverflow.ellipsis,), - subtitle: Text( - 'File Size: ${(sizeKbs / 1024).floor()} MB'), + title: Text( + 'File Name: ${_selectedFile.path.split('/').last}', + overflow: TextOverflow.ellipsis, + ), + subtitle: Text('File Size: ${(sizeKbs / 1024).floor()} MB'), ), ElevatedButton( onPressed: () => FilePreview.openFile(_selectedFile.path), @@ -186,7 +261,6 @@ class _MessagePanelState extends State { ], ), actions: [ - TextButton( onPressed: () { Navigator.of(context).pop(); @@ -195,10 +269,9 @@ class _MessagePanelState extends State { ), IconButton( onPressed: () { - Navigator.pop(context); - _sendFileMessage(context, _selectedFile); - - }, + Navigator.pop(context); + _sendFileMessage(context, _selectedFile); + }, icon: const Icon( Icons.send, ), @@ -210,9 +283,7 @@ class _MessagePanelState extends State { } } - -/// This function is used to send the file message. - void _sendFileMessage(BuildContext context, File file) async{ + void _sendFileMessage(BuildContext context, File file) async { var msgId = nanoid(21); String fileName = _selectedFile.path.split('/').last; @@ -247,7 +318,5 @@ class _MessagePanelState extends State { Msg(data, "sent", date, msgId), widget.converser, ); - } - } diff --git a/lib/encyption/rsa.dart b/lib/encyption/rsa.dart index 042b2f3..82ef802 100644 --- a/lib/encyption/rsa.dart +++ b/lib/encyption/rsa.dart @@ -1,19 +1,17 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:asn1lib/asn1lib.dart'; -import 'package:pointycastle/src/platform_check/platform_check.dart'; -import "package:pointycastle/export.dart"; +import 'package:pointycastle/export.dart'; +import 'package:pointycastle/random/fortuna_random.dart'; +import 'dart:math'; -AsymmetricKeyPair generateRSAkeyPair( - SecureRandom secureRandom, - {int bitLength = 2048}) { +AsymmetricKeyPair generateRSAkeyPair(SecureRandom secureRandom, {int bitLength = 2048}) { final keyGen = RSAKeyGenerator() ..init(ParametersWithRandom( RSAKeyGeneratorParameters(BigInt.parse('65537'), bitLength, 64), secureRandom)); final pair = keyGen.generateKeyPair(); - final myPublic = pair.publicKey as RSAPublicKey; final myPrivate = pair.privateKey as RSAPrivateKey; @@ -21,53 +19,49 @@ AsymmetricKeyPair generateRSAkeyPair( } SecureRandom exampleSecureRandom() { - final secureRandom = FortunaRandom() - ..seed(KeyParameter( - Platform.instance.platformEntropySource().getBytes(32))); + final secureRandom = FortunaRandom(); + final seedSource = Random.secure(); + final seed = List.generate(32, (_) => seedSource.nextInt(256)); + secureRandom.seed(KeyParameter(Uint8List.fromList(seed))); return secureRandom; } - Uint8List rsaEncrypt(RSAPublicKey myPublic, Uint8List dataToEncrypt) { final encryptor = OAEPEncoding(RSAEngine()) - ..init(true, PublicKeyParameter(myPublic)); // true=encrypt + ..init(true, PublicKeyParameter(myPublic)); return _processInBlocks(encryptor, dataToEncrypt); } Uint8List rsaDecrypt(RSAPrivateKey myPrivate, Uint8List cipherText) { final decryptor = OAEPEncoding(RSAEngine()) - ..init(false, PrivateKeyParameter(myPrivate)); // false=decrypt + ..init(false, PrivateKeyParameter(myPrivate)); return _processInBlocks(decryptor, cipherText); } - Uint8List _processInBlocks(AsymmetricBlockCipher engine, Uint8List input) { - final numBlocks = input.length ~/ engine.inputBlockSize + - ((input.length % engine.inputBlockSize != 0) ? 1 : 0); - - final output = Uint8List(numBlocks * engine.outputBlockSize); + final inputBlockSize = engine.inputBlockSize; + final outputBlockSize = engine.outputBlockSize; + final numBlocks = (input.length / inputBlockSize).ceil(); + final output = Uint8List(numBlocks * outputBlockSize); var inputOffset = 0; var outputOffset = 0; + while (inputOffset < input.length) { - final chunkSize = (inputOffset + engine.inputBlockSize <= input.length) - ? engine.inputBlockSize + final chunkSize = (input.length - inputOffset > inputBlockSize) + ? inputBlockSize : input.length - inputOffset; outputOffset += engine.processBlock( input, inputOffset, chunkSize, output, outputOffset); - inputOffset += chunkSize; } - return (output.length == outputOffset) - ? output - : output.sublist(0, outputOffset); + return output.sublist(0, outputOffset); } - String encodePrivateKeyToPem(RSAPrivateKey privateKey) { final topLevel = ASN1Sequence(); topLevel.add(ASN1Integer(BigInt.from(0))); @@ -93,29 +87,26 @@ String encodePublicKeyToPem(RSAPublicKey publicKey) { return "-----BEGIN PUBLIC KEY-----\r\n$dataBase64\r\n-----END PUBLIC KEY-----"; } -//parsePrivateKeyFromPem RSAPrivateKey parsePrivateKeyFromPem(String pem) { - final data = pem.split(RegExp(r'\r?\n')); - final raw = base64.decode(data.sublist(1, data.length - 1).join('')); + final data = pem.split(RegExp(r'\r?\n')).where((line) => !line.contains('-----')).join(''); + final raw = base64.decode(data); final topLevel = ASN1Sequence.fromBytes(raw); - final n = topLevel.elements[1] as ASN1Integer; - final d = topLevel.elements[3] as ASN1Integer; - final p = topLevel.elements[4] as ASN1Integer; - final q = topLevel.elements[5] as ASN1Integer; + final n = (topLevel.elements[1] as ASN1Integer).valueAsBigInteger; + final d = (topLevel.elements[3] as ASN1Integer).valueAsBigInteger; + final p = (topLevel.elements[4] as ASN1Integer).valueAsBigInteger; + final q = (topLevel.elements[5] as ASN1Integer).valueAsBigInteger; - return RSAPrivateKey( - n.valueAsBigInteger, d.valueAsBigInteger, p.valueAsBigInteger, q.valueAsBigInteger); + return RSAPrivateKey(n, d, p, q); } RSAPublicKey parsePublicKeyFromPem(String pem) { - final data = pem.split(RegExp(r'\r?\n')); - final raw = base64.decode(data.sublist(1, data.length - 1).join('')); + final data = pem.split(RegExp(r'\r?\n')).where((line) => !line.contains('-----')).join(''); + final raw = base64.decode(data); final topLevel = ASN1Sequence.fromBytes(raw); - final modulus = topLevel.elements[0] as ASN1Integer; - final exponent = topLevel.elements[1] as ASN1Integer; + final modulus = (topLevel.elements[0] as ASN1Integer).valueAsBigInteger; + final exponent = (topLevel.elements[1] as ASN1Integer).valueAsBigInteger; - return RSAPublicKey(modulus.valueAsBigInteger, exponent.valueAsBigInteger); + return RSAPublicKey(modulus, exponent); } - diff --git a/lib/pages/chat_page.dart b/lib/pages/chat_page.dart index d2883ad..9eb4600 100644 --- a/lib/pages/chat_page.dart +++ b/lib/pages/chat_page.dart @@ -1,5 +1,4 @@ import 'dart:typed_data'; - import 'package:bubble/bubble.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; @@ -10,65 +9,53 @@ import '../classes/global.dart'; import 'dart:convert'; import 'package:pointycastle/asymmetric/api.dart'; import '../components/view_file.dart'; -import '../encyption/rsa.dart'; +import '../encryption/rsa.dart'; class ChatPage extends StatefulWidget { String converser; - ChatPage({Key? key, required this.converser}) : super(key: key); - - + ChatPage({Key? key, required this.converser}) : super(key: key); @override _ChatPageState createState() => _ChatPageState(); } class _ChatPageState extends State { + final ScrollController _scrollController = ScrollController(); List messageList = []; - TextEditingController myController = TextEditingController(); @override void initState() { super.initState(); - // Adding listener to listen for profile name updates + _subscribeToProfileUpdates(); + } + + void _subscribeToProfileUpdates() { Global.profileNameStream.listen((updatedName) { - setState(() { - if (widget.converser == Global.myName) { - // Update the converser name if it matches the updated profile name + if (widget.converser == Global.myName) { + setState(() { widget.converser = updatedName; - } - }); + }); + } }); } - final ScrollController _scrollController = ScrollController(); - - @override - Widget build(BuildContext context) { - if (Provider.of(context).conversations[widget.converser] != null) { - messageList = []; - Provider.of(context) - .conversations[widget.converser]! - .forEach((key, value) { - messageList.add(value); - }); - + void _scrollToBottom() { + WidgetsBinding.instance.addPostFrameCallback((_) { if (_scrollController.hasClients) { _scrollController.animateTo( - _scrollController.position.maxScrollExtent + 50, + _scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: 300), curve: Curves.easeOut, ); } - } + }); + } - Map> groupedMessages = {}; - for (var msg in messageList) { - String date = DateFormat('dd/MM/yyyy').format(DateTime.parse(msg.timestamp)); - if (groupedMessages[date] == null) { - groupedMessages[date] = []; - } - groupedMessages[date]!.add(msg); - } + @override + Widget build(BuildContext context) { + messageList = _getMessageList(context); + + Map> groupedMessages = _groupMessagesByDate(messageList); return Scaffold( appBar: AppBar( @@ -78,77 +65,14 @@ class _ChatPageState extends State { children: [ Expanded( child: messageList.isEmpty - ? const Center( - child: Text('No messages yet'), - ) + ? const Center(child: Text('No messages yet')) : ListView.builder( controller: _scrollController, padding: const EdgeInsets.all(8), itemCount: groupedMessages.keys.length, - itemBuilder: (BuildContext context, int index) { + itemBuilder: (context, index) { String date = groupedMessages.keys.elementAt(index); - return Column( - children: [ - Center( - child: Padding( - padding: const EdgeInsets.only(top: 10), - child: Text( - date, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - ), - ), - ...groupedMessages[date]!.map((msg) { - String displayMessage = msg.message; - if (Global.myPrivateKey != null) { - RSAPrivateKey privateKey = Global.myPrivateKey!; - dynamic data = jsonDecode(msg.message); - if (data['type'] == 'text') { - Uint8List encryptedBytes = base64Decode(data['data']); - Uint8List decryptedBytes = rsaDecrypt(privateKey, encryptedBytes); - displayMessage = utf8.decode(decryptedBytes); - } - } - return Column( - crossAxisAlignment: msg.msgtype == 'sent' - ? CrossAxisAlignment.end - : CrossAxisAlignment.start, - children: [ - Align( - alignment: msg.msgtype == 'sent' - ? Alignment.centerRight - : Alignment.centerLeft, - child: Bubble( - padding: const BubbleEdges.all(12), - margin: const BubbleEdges.only(top: 10), - style: BubbleStyle( - elevation: 3, - shadowColor: Colors.black.withOpacity(0.5), - ), - radius: const Radius.circular(10), - color: msg.msgtype == 'sent' - ? const Color(0xffd1c4e9) - : const Color(0xff80DEEA), - child: msg.message.contains('file') - ? _buildFileBubble(msg) - : Text( - displayMessage, - style: const TextStyle(color: Colors.black87), - ), - ), - ), - Padding( - padding: const EdgeInsets.only(top: 2, bottom: 10), - child: Text( - dateFormatter(timeStamp: msg.timestamp), - style: const TextStyle(color: Colors.black54, fontSize: 10), - ), - ), - ], - ); - }), - ], - ); + return _buildMessageGroup(date, groupedMessages[date]!); }, ), ), @@ -158,6 +82,83 @@ class _ChatPageState extends State { ); } + List _getMessageList(BuildContext context) { + var conversation = Provider.of(context).conversations[widget.converser]; + if (conversation == null) return []; + return conversation.values.toList(); + } + + Map> _groupMessagesByDate(List messages) { + Map> groupedMessages = {}; + for (var msg in messages) { + String date = DateFormat('dd/MM/yyyy').format(DateTime.parse(msg.timestamp)); + groupedMessages.putIfAbsent(date, () => []).add(msg); + } + return groupedMessages; + } + + Widget _buildMessageGroup(String date, List messages) { + return Column( + children: [ + _buildDateHeader(date), + ...messages.map((msg) => _buildMessageBubble(msg)), + ], + ); + } + + Widget _buildDateHeader(String date) { + return Center( + child: Padding( + padding: const EdgeInsets.only(top: 10), + child: Text( + date, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ), + ); + } + + Widget _buildMessageBubble(Msg msg) { + String displayMessage = msg.message; + if (msg.msgtype == 'text' && Global.myPrivateKey != null) { + displayMessage = _decryptMessage(msg.message); + } + return Column( + crossAxisAlignment: msg.msgtype == 'sent' + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + children: [ + Align( + alignment: msg.msgtype == 'sent' + ? Alignment.centerRight + : Alignment.centerLeft, + child: Bubble( + padding: const BubbleEdges.all(12), + margin: const BubbleEdges.only(top: 10), + style: BubbleStyle( + elevation: 3, + shadowColor: Colors.black.withOpacity(0.5), + ), + radius: const Radius.circular(10), + color: msg.msgtype == 'sent' + ? const Color(0xffd1c4e9) + : const Color(0xff80DEEA), + child: msg.message.contains('file') + ? _buildFileBubble(msg) + : Text(displayMessage, style: const TextStyle(color: Colors.black87)), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 2, bottom: 10), + child: Text( + dateFormatter(timeStamp: msg.timestamp), + style: const TextStyle(color: Colors.black54, fontSize: 10), + ), + ), + ], + ); + } + Widget _buildFileBubble(Msg msg) { dynamic data = jsonDecode(msg.message); String fileName = data['fileName']; @@ -168,26 +169,32 @@ class _ChatPageState extends State { Flexible( child: Text( fileName, - style: const TextStyle( - color: Colors.black87, - ), - overflow: TextOverflow.visible, + style: const TextStyle(color: Colors.black87), + overflow: TextOverflow.ellipsis, ), ), IconButton( icon: const Icon(Icons.file_open, color: Colors.black87), - onPressed: () { - FilePreview.openFile(filePath); - }, + onPressed: () => FilePreview.openFile(filePath), ), ], ); } + + String _decryptMessage(String message) { + try { + RSAPrivateKey privateKey = Global.myPrivateKey!; + dynamic data = jsonDecode(message); + Uint8List encryptedBytes = base64Decode(data['data']); + Uint8List decryptedBytes = rsaDecrypt(privateKey, encryptedBytes); + return utf8.decode(decryptedBytes); + } catch (e) { + return "[Error decrypting message]"; + } + } } String dateFormatter({required String timeStamp}) { DateTime dateTime = DateTime.parse(timeStamp); - String formattedTime = DateFormat('hh:mm aa').format(dateTime); - return formattedTime; + return DateFormat('hh:mm aa').format(dateTime); } - diff --git a/pubspec.yaml b/pubspec.yaml index 70a4a2f..b67840c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -33,6 +33,7 @@ dependencies: permission_handler: ^11.3.1 path_provider: ^2.1.4 web_socket_channel: ^3.0.1 + flutter_sound: ^9.6.0 dev_dependencies: flutter_lints: From 89e0a9be3858ab62bfa428386deeb8190ed0f937 Mon Sep 17 00:00:00 2001 From: dikshantbhangala Date: Tue, 14 Jan 2025 22:58:53 +0530 Subject: [PATCH 4/5] Added chat history export with media support and UI improvements --- android/build.gradle | 11 ++++ android/gradle.properties | 1 + .../gradle/wrapper/gradle-wrapper.properties | 3 +- lib/encyption/rsa.dart | 59 +++++++++++++++++++ lib/pages/chat_page.dart | 13 +++- macos/Flutter/GeneratedPluginRegistrant.swift | 4 ++ pubspec.yaml | 12 +++- 7 files changed, 98 insertions(+), 5 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 70a1de5..9bb29bc 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,5 +1,16 @@ buildscript { ext.kotlin_version = '2.0.10' + + repositories { + google() + mavenCentral() + } + + dependencies { + // Add the Android Gradle Plugin classpath + classpath 'com.android.tools.build:gradle:8.1.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } } allprojects { diff --git a/android/gradle.properties b/android/gradle.properties index 3b5b324..5a505fa 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,3 +1,4 @@ org.gradle.jvmargs=-Xmx4G -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true android.enableJetifier=true +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index e1ca574..9f7d524 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip + diff --git a/lib/encyption/rsa.dart b/lib/encyption/rsa.dart index 82ef802..bac4556 100644 --- a/lib/encyption/rsa.dart +++ b/lib/encyption/rsa.dart @@ -1,9 +1,16 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:asn1lib/asn1lib.dart'; +import 'package:pointycastle/api.dart'; import 'package:pointycastle/export.dart'; import 'package:pointycastle/random/fortuna_random.dart'; import 'dart:math'; +import 'dart:io'; +import 'package:path_provider/path_provider.dart'; +import 'package:pdf/pdf.dart'; +import 'package:pdf/widgets.dart' as pw; +import 'package:sqflite/sqflite.dart'; +import 'package:path/path.dart'; AsymmetricKeyPair generateRSAkeyPair(SecureRandom secureRandom, {int bitLength = 2048}) { final keyGen = RSAKeyGenerator() @@ -110,3 +117,55 @@ RSAPublicKey parsePublicKeyFromPem(String pem) { return RSAPublicKey(modulus, exponent); } +Future initDatabase() async { + final databasePath = await getDatabasesPath(); + final path = join(databasePath, 'chat_database.db'); + + return openDatabase( + path, + onCreate: (db, version) { + return db.execute( + 'CREATE TABLE messages(id INTEGER PRIMARY KEY, content TEXT, mediaPath TEXT)', + ); + }, + version: 1, + ); +} + +Future saveMessage(String message, {String? mediaPath}) async { + final db = await initDatabase(); + await db.insert( + 'messages', + {'content': message, 'mediaPath': mediaPath}, + conflictAlgorithm: ConflictAlgorithm.replace, + ); +} + +Future>> retrieveMessages() async { + final db = await initDatabase(); + return db.query('messages'); +} + +Future exportChatHistory() async { + final pdf = pw.Document(); + final messages = await retrieveMessages(); + + for (var message in messages) { + pdf.addPage(pw.Page(build: (pw.Context context) { + return pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Text(message['content']), + if (message['mediaPath'] != null) + pw.Text('Media: ${message['mediaPath']}'), + ], + ); + })); + } + + final directory = await getApplicationDocumentsDirectory(); + final filePath = '${directory.path}/chat_history.pdf'; + final file = File(filePath); + await file.writeAsBytes(await pdf.save()); + print('Chat history exported to: $filePath'); +} diff --git a/lib/pages/chat_page.dart b/lib/pages/chat_page.dart index 9eb4600..a94cb7e 100644 --- a/lib/pages/chat_page.dart +++ b/lib/pages/chat_page.dart @@ -9,7 +9,7 @@ import '../classes/global.dart'; import 'dart:convert'; import 'package:pointycastle/asymmetric/api.dart'; import '../components/view_file.dart'; -import '../encryption/rsa.dart'; +import '../encyption/rsa.dart'; class ChatPage extends StatefulWidget { String converser; @@ -60,6 +60,17 @@ class _ChatPageState extends State { return Scaffold( appBar: AppBar( title: Text(widget.converser), + actions: [ + IconButton( + icon: Icon(Icons.download), + onPressed: () async { + await exportChatHistory(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Chat history exported successfully!')), + ); + }, + ), + ], ), body: Column( children: [ diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 9d4b458..c1c43ce 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,14 +5,18 @@ import FlutterMacOS import Foundation +import audio_session import flutter_secure_storage_macos +import just_audio import local_auth_darwin import path_provider_foundation import shared_preferences_foundation import sqflite_darwin func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) + JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin")) FLALocalAuthPlugin.register(with: registry.registrar(forPlugin: "FLALocalAuthPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) diff --git a/pubspec.yaml b/pubspec.yaml index b67840c..d172e49 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,7 +18,6 @@ dependencies: flutter_nearby_connections: ^1.1.2 cupertino_icons: ^1.0.8 bubble: ^1.2.1 - nanoid: ^1.0.0 sqflite: ^2.3.3+1 intl: ^0.19.0 pointycastle: ^3.9.1 @@ -32,13 +31,20 @@ dependencies: open_filex: ^4.5.0 permission_handler: ^11.3.1 path_provider: ^2.1.4 - web_socket_channel: ^3.0.1 - flutter_sound: ^9.6.0 + web_socket_channel: ^2.2.0 + nanoid: ^1.0.0 + flutter_sound: any + just_audio: ^0.9.14 + pdf: ^3.11.1 + dev_dependencies: flutter_lints: flutter_test: sdk: flutter + + + flutter: uses-material-design: true \ No newline at end of file From 5dbe54fb0bf7f0fab25f1c20758d44d38e4c974b Mon Sep 17 00:00:00 2001 From: dikshantbhangala Date: Wed, 22 Jan 2025 18:48:52 +0530 Subject: [PATCH 5/5] Implemented real-time file transfer progress tracking and UI updates --- lib/classes/global.dart | 49 ++++++++------ lib/components/view_file.dart | 85 ++++++++++++------------- lib/connections/connection_handler.dart | 64 +++++++++++++++++++ lib/pages/chat_page.dart | 37 ++++++++++- lib/pages/file_transfer_logic.dart | 81 +++++++++++++++++++++++ 5 files changed, 252 insertions(+), 64 deletions(-) create mode 100644 lib/connections/connection_handler.dart create mode 100644 lib/pages/file_transfer_logic.dart diff --git a/lib/classes/global.dart b/lib/classes/global.dart index fb4d39f..30d7210 100644 --- a/lib/classes/global.dart +++ b/lib/classes/global.dart @@ -11,7 +11,6 @@ import '../database/database_helper.dart'; import '../p2p/adhoc_housekeeping.dart'; import 'msg.dart'; - class Global extends ChangeNotifier { static RSAPrivateKey? myPrivateKey; static RSAPublicKey? myPublicKey; @@ -28,18 +27,19 @@ class Global extends ChangeNotifier { static Map cache = {}; static final GlobalKey scaffoldKey = GlobalKey(); - static var profileNameStream; + static double fileTransferProgress = 0.0; // Progress percentage + static double transferSpeed = 0.0; // Speed in bytes/second + static int estimatedTimeRemaining = 0; // Time in seconds + static bool isTransferActive = false; + static var profileNameStream; void sentToConversations(Msg msg, String converser, {bool addToTable = true}) { - - conversations.putIfAbsent(converser, () => {}); conversations[converser]![msg.id] = msg; if (addToTable) { insertIntoConversationsTable(msg, converser); } - notifyListeners(); broadcast(scaffoldKey.currentContext!); } @@ -51,8 +51,8 @@ class Global extends ChangeNotifier { print("Received Message: $message"); } - //file decoding and saving - if(message['type'] == 'file') { + // File decoding and saving + if (message['type'] == 'file') { String filePath = await decodeAndStoreFile( message['data'], message['fileName']); conversations.putIfAbsent(sender, () => {}); @@ -69,8 +69,7 @@ class Global extends ChangeNotifier { insertIntoConversationsTable(msg, sender); notifyListeners(); } - } - else { + } else { conversations.putIfAbsent(sender, () => {}); if (!conversations[sender]!.containsKey(decodedMessage['id'])) { var msg = Msg( @@ -86,14 +85,10 @@ class Global extends ChangeNotifier { Future decodeAndStoreFile(String encodedFile, String fileName) async { Uint8List fileBytes = base64.decode(encodedFile); - //to send files encrypted using RSA - // Uint8List fileData = rsaDecrypt(Global.myPrivateKey!, fileBytes); - - Directory documents ; + Directory documents; if (Platform.isAndroid) { documents = (await getExternalStorageDirectory())!; - } - else { + } else { documents = await getApplicationDocumentsDirectory(); } PermissionStatus status = await Permission.storage.request(); @@ -104,15 +99,33 @@ class Global extends ChangeNotifier { print("File saved at: $path"); } return path; - } - else { + } else { throw const FileSystemException('Storage permission not granted'); } } + void updateFileTransferProgress(int bytesTransferred, int totalBytes) { + fileTransferProgress = (bytesTransferred / totalBytes) * 100; - void updateDevices(List devices) { + // Calculate transfer speed + transferSpeed = bytesTransferred / 1024; // Speed in KB/s + + // Estimate time remaining + estimatedTimeRemaining = + ((totalBytes - bytesTransferred) / transferSpeed).round(); + + notifyListeners(); + } + void resetFileTransferProgress() { + fileTransferProgress = 0.0; + transferSpeed = 0.0; + estimatedTimeRemaining = 0; + isTransferActive = false; + notifyListeners(); + } + + void updateDevices(List devices) { this.devices = devices; notifyListeners(); } diff --git a/lib/components/view_file.dart b/lib/components/view_file.dart index 2e491c3..962c251 100644 --- a/lib/components/view_file.dart +++ b/lib/components/view_file.dart @@ -2,79 +2,78 @@ import 'dart:io'; import 'package:open_filex/open_filex.dart'; import 'package:permission_handler/permission_handler.dart'; - class FilePreview { - + /// Opens a file based on the platform and file path. static Future openFile(String path) async { if (Platform.isIOS) { - _openIOSFile(path); + await _openFileIOS(path); } else if (Platform.isAndroid) { if (path.contains("Android/data")) { - _openAndroidPrivateFile(path); + await _openAndroidPrivateFile(path); } else if (path.contains("DCIM")) { - if (path.contains(".jpg")) { - _openAndroidExternalImage(path); - } else if (path.contains(".mp4")) { - _openAndroidExternalVideo(path); - } else if (path.contains(".mp3")) { - _openAndroidExternalAudio(path); + if (path.endsWith(".jpg") || path.endsWith(".jpeg") || path.endsWith(".png")) { + await _openAndroidExternalImage(path); + } else if (path.endsWith(".mp4")) { + await _openAndroidExternalVideo(path); + } else if (path.endsWith(".mp3") || path.endsWith(".wav")) { + await _openAndroidExternalAudio(path); } else { - _openAndroidExternalFile(path); + await _openAndroidExternalFile(path); } } else { - _openAndroidOtherAppFile(path); + await _openAndroidOtherAppFile(path); } } } - // ignore: unused_element - static _openIOSFile(String path) async { - await OpenFilex.open( path); - + /// Opens a file on iOS. + static Future _openFileIOS(String path) async { + await OpenFilex.open(path); } - // ignore: unused_element - static _openAndroidPrivateFile( String path) async { - await OpenFilex.open( path); - - + /// Opens a private file on Android (e.g., within `Android/data`). + static Future _openAndroidPrivateFile(String path) async { + await OpenFilex.open(path); } - static _openAndroidOtherAppFile(String path) async { - if (await Permission.manageExternalStorage.request().isGranted) { - await OpenFilex.open(path); + /// Opens a file in another app on Android (if outside private folders). + static Future _openAndroidOtherAppFile(String path) async { + if (await _requestPermission(Permission.manageExternalStorage)) { + await OpenFilex.open(path); } } - // ignore: unused_element - static _openAndroidExternalImage(String path) async { - if (await Permission.photos.request().isGranted) { - await OpenFilex.open(path); - + /// Opens an external image file on Android. + static Future _openAndroidExternalImage(String path) async { + if (await _requestPermission(Permission.photos)) { + await OpenFilex.open(path); } } - // ignore: unused_element - static _openAndroidExternalVideo(String path) async { - if (await Permission.videos.request().isGranted) { + /// Opens an external video file on Android. + static Future _openAndroidExternalVideo(String path) async { + if (await _requestPermission(Permission.videos)) { await OpenFilex.open(path); - } } - // ignore: unused_element - static _openAndroidExternalAudio(String path) async { - if (await Permission.audio.request().isGranted) { -await OpenFilex.open(path); - + /// Opens an external audio file on Android. + static Future _openAndroidExternalAudio(String path) async { + if (await _requestPermission(Permission.audio)) { + await OpenFilex.open(path); } } - // ignore: unused_element - static _openAndroidExternalFile(String path) async { - if (await Permission.manageExternalStorage.request().isGranted) { + /// Opens any external file on Android (requires manage storage permission). + static Future _openAndroidExternalFile(String path) async { + if (await _requestPermission(Permission.manageExternalStorage)) { await OpenFilex.open(path); - } } -} \ No newline at end of file + + /// Requests a specific permission and returns whether it is granted. + static Future _requestPermission(Permission permission) async { + final status = await permission.request(); + return status.isGranted; + } +} diff --git a/lib/connections/connection_handler.dart b/lib/connections/connection_handler.dart new file mode 100644 index 0000000..766a057 --- /dev/null +++ b/lib/connections/connection_handler.dart @@ -0,0 +1,64 @@ +import 'dart:async'; +import 'package:flutter_nearby_connections/flutter_nearby_connections.dart'; + +class ConnectionHandler { + final NearbyService nearbyService = NearbyService(); + final List devices = []; + final List connectedDevices = []; + StreamSubscription? deviceSubscription; + StreamSubscription? receivedDataSubscription; + + /// Initialize the Nearby Service + Future initialize({required String userName}) async { + await nearbyService.init( + serviceType: 'your_service_type', // Replace with your service type. + deviceName: userName, + strategy: Strategy.P2P_CLUSTER, callback: null, + ); + await nearbyService.startAdvertisingPeer(); + await nearbyService.startBrowsingForPeers(); + + _listenToDeviceChanges(); + _listenToReceivedData(); + } + + /// Listen for changes in available devices. + void _listenToDeviceChanges() { + deviceSubscription = nearbyService.stateChangedSubscription(callback: (devicesList) { + devices + ..clear() + ..addAll(devicesList); + + // Filter connected devices + connectedDevices + ..clear() + ..addAll(devicesList.where((device) => device.state == SessionState.connected)); + }); + } + + /// Listen for received data from peers. + void _listenToReceivedData() { + receivedDataSubscription = nearbyService.dataReceivedSubscription(callback: (data) { + // Handle received data here + print("Data received: ${data['message']}"); + }); + } + + /// Send a message to a specific device. + Future sendMessage(Device device, String message) async { + await nearbyService.sendMessage(device.deviceId, message); + } + + /// Disconnect from a device. + Future disconnectDevice(Device device) async { + await nearbyService.disconnectPeer(deviceID: device.deviceId); + } + + /// Cleanup resources when done. + Future dispose() async { + await deviceSubscription?.cancel(); + await receivedDataSubscription?.cancel(); + await nearbyService.stopAdvertisingPeer(); + await nearbyService.stopBrowsingForPeers(); + } +} diff --git a/lib/pages/chat_page.dart b/lib/pages/chat_page.dart index a94cb7e..790ef8b 100644 --- a/lib/pages/chat_page.dart +++ b/lib/pages/chat_page.dart @@ -22,6 +22,8 @@ class ChatPage extends StatefulWidget { class _ChatPageState extends State { final ScrollController _scrollController = ScrollController(); List messageList = []; + double fileTransferProgress = 0.0; + bool isTransferring = false; @override void initState() { @@ -51,6 +53,29 @@ class _ChatPageState extends State { }); } + Future _startFileTransfer(String filePath) async { + setState(() { + isTransferring = true; + fileTransferProgress = 0.0; + }); + + // Simulating file transfer with a loop for progress update + for (int i = 0; i <= 100; i++) { + await Future.delayed(const Duration(milliseconds: 50)); // Simulate transfer delay + setState(() { + fileTransferProgress = i / 100.0; + }); + } + + setState(() { + isTransferring = false; + }); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('File transfer completed successfully!')), + ); + } + @override Widget build(BuildContext context) { messageList = _getMessageList(context); @@ -62,11 +87,11 @@ class _ChatPageState extends State { title: Text(widget.converser), actions: [ IconButton( - icon: Icon(Icons.download), + icon: const Icon(Icons.download), onPressed: () async { await exportChatHistory(); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Chat history exported successfully!')), + const SnackBar(content: Text('Chat history exported successfully!')), ); }, ), @@ -87,6 +112,12 @@ class _ChatPageState extends State { }, ), ), + if (isTransferring) + LinearProgressIndicator( + value: fileTransferProgress, + backgroundColor: Colors.grey.shade200, + valueColor: AlwaysStoppedAnimation(Colors.blue), + ), MessagePanel(converser: widget.converser), ], ), @@ -186,7 +217,7 @@ class _ChatPageState extends State { ), IconButton( icon: const Icon(Icons.file_open, color: Colors.black87), - onPressed: () => FilePreview.openFile(filePath), + onPressed: () => _startFileTransfer(filePath), ), ], ); diff --git a/lib/pages/file_transfer_logic.dart b/lib/pages/file_transfer_logic.dart new file mode 100644 index 0000000..79ece22 --- /dev/null +++ b/lib/pages/file_transfer_logic.dart @@ -0,0 +1,81 @@ +import 'dart:async'; +import 'dart:io'; + +class FileTransferService { + /// StreamController for transfer progress + final StreamController _progressController = StreamController.broadcast(); + + /// Get the progress stream + Stream get progressStream => _progressController.stream; + + /// Upload a file with progress tracking + Future uploadFile({ + required File file, + required Function onComplete, + required Function(String error) onError, + }) async { + try { + final totalBytes = await file.length(); + int bytesTransferred = 0; + + // Simulating chunked file upload + final chunkSize = 1024 * 512; // 512 KB + final fileStream = file.openRead(); + + await for (final chunk in fileStream) { + bytesTransferred += chunk.length; + + // Update progress + final progress = bytesTransferred / totalBytes; + _progressController.add(progress); + + // Simulate sending the chunk + await Future.delayed(const Duration(milliseconds: 100)); + } + + // Notify completion + onComplete(); + } catch (e) { + // Notify error + onError(e.toString()); + } finally { + _progressController.close(); + } + } + + /// Download a file with progress tracking + Future downloadFile({ + required String savePath, + required int totalBytes, + required Stream> incomingStream, + required Function onComplete, + required Function(String error) onError, + }) async { + try { + final file = File(savePath).openWrite(); + int bytesReceived = 0; + + // Write incoming chunks to the file + await for (final chunk in incomingStream) { + bytesReceived += chunk.length; + file.add(chunk); + + // Update progress + final progress = bytesReceived / totalBytes; + _progressController.add(progress); + } + + await file.close(); + onComplete(); + } catch (e) { + onError(e.toString()); + } finally { + _progressController.close(); + } + } + + /// Dispose the StreamController + void dispose() { + _progressController.close(); + } +}