diff --git a/Contribution_Guidelines.md b/Contribution_Guidelines.md new file mode 100644 index 0000000..b36e4b6 --- /dev/null +++ b/Contribution_Guidelines.md @@ -0,0 +1,40 @@ +## Steps to Set Up Open Peer Chat + +1. **Fork the Repository** + - Navigate to the [Open Peer Chat GitHub Repository](https://github.com/AOSSIE-Org/OpenPeerChat-flutter). + - Click the **Fork** button in the top-right corner to create your own copy of the repository. + +2. **Clone Your Forked Repository** + ```bash + git clone https://github.com/YOUR_USERNAME/OpenPeerChat-flutter.git + cd OpenPeerChat-flutter + ``` + +3. **Make Your Changes** + - Modify the code to implement the required feature or resolve the issue. + +4. **Commit Your Changes** + - Stage your changes and commit them with a meaningful message. + ```bash + git add . + git commit -m "Add: Brief description of your update" + ``` + +5. **Push Your Changes** + - Push your changes to your forked repository. + ```bash + git push origin main + ``` + +6. **Create a Pull Request** + - Go to your forked repository on GitHub. + - Click the **Compare & Pull Request** button. + - Add a detailed description of the changes you made. + - Include a link to a demo video showcasing the feature you added or the issue you resolved. + - Submit the pull request for review. + +### Additional Notes +- Contributors should make changes directly to the `main` branch of their forked repository. +- Ensure that your code adheres to the project’s coding standards and passes all necessary tests before creating a pull request. +- Provide a clear and concise description of your changes in the pull request, along with screenshots or video demonstrations if applicable. +- Keep your forked repository up-to-date with the latest changes from the `main` branch to avoid conflicts. diff --git a/README.md b/README.md index a8a0fc9..b63c69a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ # Peer-to-Peer Messaging Application +# LATEST Updates (GSOC 2024) - +https://github.com/AOSSIE-Org/OpenPeerChat-flutter/blob/main/GSOC/2024/Bhavik_Mangla.md + +# Contribution Guidelines - +https://github.com/AOSSIE-Org/OpenPeerChat-flutter/blob/main/Contribution_Guidelines.md + GSoC pitch 2021. # Chosen Idea: A message sending/relaying messages to nearby devices until the destination is reached, instead of relying on a central server. GPS positioning could be used to route messages along the shortest path. Right now, despite the use of end-to-end encryption, our best and most popular messaging apps still rely on central servers to intermediate the communication. This has disadvantages such as: diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index eb48cb0..ee8455e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,22 +1,34 @@ - + - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + android:icon="@mipmap/ic_launcher" + android:enableOnBackInvokedCallback="true"> + + /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -214,26 +236,21 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 6286E038E4D2E640F9A73383 /* [CP] Check Pods Manifest.lock */ = { + 623560887A14F11A592E14A2 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; + name = "[CP] Copy Pods Resources"; outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; showEnvVarsInLog = 0; }; 9740EEB61CF901F6004384FC /* Run Script */ = { @@ -251,7 +268,7 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - D0A49172F0D59151F50A8FA4 /* [CP] Embed Pods Frameworks */ = { + BC561ADEFDA59CE89BC36ABB /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -268,23 +285,6 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - FFA8B1284F3221840EE75321 /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Copy Pods Resources"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index e67b280..4f74653 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -50,6 +50,7 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index d9e3dba..9540c38 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -2,15 +2,8 @@ - NSBonjourServices - - _mp-connection._tcp - - UIRequiresPersistentWiFi + CADisableMinimumFrameDurationOnPhone - NSBluetoothAlwaysUsageDescription - nearby connections - CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable @@ -31,10 +24,30 @@ $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS + NSBluetoothAlwaysUsageDescription + nearby connections + NSBonjourServices + + _mp-connection._tcp + + NSFaceIDUsageDescription + Why is my app authenticating using face id? + NSMicrophoneUsageDescription + OpenPeerChat requires microphone access to record voice messages + NSPhotoLibraryUsageDescription + OpenPeerChat needs access to save voice messages + UIApplicationSupportsIndirectInputEvents + + UIBackgroundModes + + audio + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main + UIRequiresPersistentWiFi + UISupportedInterfaceOrientations UIInterfaceOrientationPortrait @@ -50,11 +63,5 @@ UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - - NSFaceIDUsageDescription - Why is my app authenticating using face id? diff --git a/lib/classes/global.dart b/lib/classes/global.dart index 6af0a7b..bf78be9 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}) { @@ -50,13 +52,18 @@ class Global extends ChangeNotifier { } //file decoding and saving - if(message['type'] == 'file') { - String filePath = await decodeAndStoreFile( - message['data'], message['fileName']); + if (message['type'] == 'voice' || message['type'] == 'file') { + final String filePath = await decodeAndStoreFile( + message['data'], + message['fileName'], + isVoice: message['type'] == 'voice', + ); conversations.putIfAbsent(sender, () => {}); if (!conversations[sender]!.containsKey(decodedMessage['id'])) { + print("Adding to conversations"); + print("Message: ${message['type']}"); decodedMessage['message'] = json.encode({ - 'type': 'file', + 'type': message['type'], 'filePath': filePath, 'fileName': message['fileName'] }); @@ -81,7 +88,7 @@ class Global extends ChangeNotifier { } } - Future decodeAndStoreFile(String encodedFile, String fileName) async { + Future decodeAndStoreFile(String encodedFile, String fileName, {bool isVoice = false}) async { Uint8List fileBytes = base64.decode(encodedFile); //to send files encrypted using RSA @@ -94,9 +101,17 @@ class Global extends ChangeNotifier { else { documents = await getApplicationDocumentsDirectory(); } + PermissionStatus status = await Permission.storage.request(); + + final String subDir = isVoice ? 'voice_messages' : 'files'; if (status.isGranted) { - final path = '${documents.path}/$fileName'; + + final Directory finalDir = Directory('${documents.path}/$subDir'); + if (!await finalDir.exists()) { + await finalDir.create(recursive: true); + } + final path ='${finalDir.path}/$fileName'; File(path).writeAsBytes(fileBytes); if (kDebugMode) { print("File saved at: $path"); diff --git a/lib/components/audio_service.dart b/lib/components/audio_service.dart new file mode 100644 index 0000000..721b446 --- /dev/null +++ b/lib/components/audio_service.dart @@ -0,0 +1,167 @@ +import 'dart:io'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:just_audio/just_audio.dart'; +import 'package:record/record.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:device_info_plus/device_info_plus.dart'; + + +class RecordingPermissionException implements Exception { + final String message; + RecordingPermissionException(this.message); + + @override + String toString() => 'RecordingPermissionException: $message'; +} + +class AudioService { + static final AudioService _instance = AudioService._internal(); + factory AudioService() => _instance; + AudioService._internal(); + + final Record _recorder = Record(); + final AudioPlayer _player = AudioPlayer(); + bool _isRecorderInitialized = false; + bool _isRecording = false; + String? _currentRecordingPath; + + bool get isRecording => _isRecording; + String? get currentRecordingPath => _currentRecordingPath; + + Future _requestPermissions() async { + try { + if (Platform.isAndroid) { + final androidInfo = await DeviceInfoPlugin().androidInfo; + if (androidInfo.version.sdkInt >= 33) { + // Check status before requesting + if (await Permission.audio.status.isDenied) { + await Permission.audio.request(); + } + if (await Permission.videos.status.isDenied) { + await Permission.videos.request(); + } + } else { + // For older Android versions + if (await Permission.storage.status.isDenied) { + await Permission.storage.request(); + } + if (await Permission.manageExternalStorage.status.isDenied) { + await Permission.manageExternalStorage.request(); + } + } + } + + // Check microphone permission + if (await Permission.microphone.status.isDenied) { + final micStatus = await Permission.microphone.request(); + if (micStatus.isPermanentlyDenied) { + // Provide user feedback + debugPrint('Microphone permission permanently denied'); + return false; + } + return micStatus.isGranted; + } + return true; + } catch (e) { + debugPrint('Permission request error: $e'); + return false; + } + } + Future initRecorder() async { + if (_isRecorderInitialized) return; + + try { + debugPrint('Initializing recorder...'); + bool permissionsGranted = await _requestPermissions(); + + if (!permissionsGranted) { + throw RecordingPermissionException( + 'Required permissions were not granted. Please enable necessary permissions in your device settings.' + ); + } + + await _recorder.hasPermission(); + _isRecorderInitialized = true; + debugPrint('Recorder initialized successfully'); + } catch (e) { + _isRecorderInitialized = false; + debugPrint('Recorder initialization failed: $e'); + rethrow; + } + } + + Future startRecording() async { + if (_isRecording) { + throw RecordingPermissionException('Already recording'); + } + + try { + if (!_isRecorderInitialized) { + await initRecorder(); + } + + Directory tempDir = await getTemporaryDirectory(); + _currentRecordingPath = '${tempDir.path}/voice_${DateTime.now().millisecondsSinceEpoch}.m4a'; + + await _recorder.start( + path: _currentRecordingPath!, + encoder: AudioEncoder.aacLc, + bitRate: 128000, + samplingRate: 44100, + ); + + _isRecording = true; + return _currentRecordingPath!; + } catch (e) { + _isRecording = false; + _currentRecordingPath = null; + rethrow; + } + } + + Future stopRecording() async { + if (!_isRecording) return null; + + try { + await _recorder.stop(); + _isRecording = false; + final recordedPath = _currentRecordingPath; + _currentRecordingPath = null; + return recordedPath; + } catch (e) { + rethrow; + } + } + + Future playRecording(String path) async { + try { + await _player.setFilePath(path); + await _player.play(); + } catch (e) { + rethrow; + } + } + + Future stopPlaying() async { + try { + await _player.stop(); + } catch (e) { + rethrow; + } + } + + Future dispose() async { + try { + if (_isRecording) { + await stopRecording(); + } + await _recorder.dispose(); + await _player.dispose(); + _isRecorderInitialized = false; + } catch (e) { + rethrow; + } + } +} diff --git a/lib/components/message_panel.dart b/lib/components/message_panel.dart index c5db219..beb22bc 100644 --- a/lib/components/message_panel.dart +++ b/lib/components/message_panel.dart @@ -2,8 +2,10 @@ 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:permission_handler/permission_handler.dart'; import 'package:pointycastle/asymmetric/api.dart'; import 'package:provider/provider.dart'; import '../classes/global.dart'; @@ -11,15 +13,12 @@ import '../classes/msg.dart'; import '../classes/payload.dart'; import '../database/database_helper.dart'; import '../encyption/rsa.dart'; +import 'audio_service.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; + const MessagePanel({Key? key, required this.converser, this.onMessageSent}) : super(key: key); + final String converser;final VoidCallback? onMessageSent; @override State createState() => _MessagePanelState(); @@ -29,18 +28,112 @@ class _MessagePanelState extends State { TextEditingController myController = TextEditingController(); File _selectedFile = File(''); + FlutterSoundRecorder? _recorder; // ADDED + bool _isRecording = false; // ADDED + String? _recordedFilePath; // ADDED + late final AudioService _audioService; + bool _isRecording = false; + String? _currentRecordingPath; + + + @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)); + } + + _audioService = AudioService(); + _initializeAudio(); + } + + Future _initializeAudio() async { + try { + await _audioService.initRecorder(); + } catch (e) { + // Show error dialog or snackbar to user + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(e.toString()), + backgroundColor: Colors.red, + action: SnackBarAction( + label: 'Settings', + onPressed: () { + openAppSettings(); // From permission_handler package + }, + ), + ), + ); + } + } + + Future _handleVoiceRecordingStart() async { + try { + _currentRecordingPath = await _audioService.startRecording(); + setState(() => _isRecording = true); + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to start recording: $e')), + ); + } + } + } + + Future _handleVoiceRecordingEnd() async { + if (!_isRecording) return; + + try { + await _audioService.stopRecording(); + setState(() => _isRecording = false); + if (_currentRecordingPath != null) { + _sendVoiceMessage(File(_currentRecordingPath!)); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to stop recording: $e')), + ); + } + } + } + + + @override + void dispose() { + myController.dispose(); + _audioService.dispose(); + super.dispose(); + } + + + @override Widget build(BuildContext context) { - return Padding( + return Container( 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,15 +142,65 @@ 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( Icons.send, ), + decoration: BoxDecoration( + color: Theme.of(context).scaffoldBackgroundColor, + boxShadow: [ + BoxShadow( + offset: const Offset(0, -1), + blurRadius: 5, + color: Colors.black.withOpacity(0.1), + ), + ], + ), + child: Row( + children: [ + Expanded( + child: TextField( + controller: myController, + maxLines: null, + decoration: const InputDecoration( + hintText: 'Type a message', + border: InputBorder.none, ), - ], + ), ), - ), + IconButton( + icon: const Icon(Icons.attach_file), + onPressed: () => _navigateToFilePreviewPage(context), + ), + GestureDetector( + onLongPressStart: (_) => _handleVoiceRecordingStart(), + onLongPressEnd: (_) => _handleVoiceRecordingEnd(), + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: _isRecording ? Colors.red.withOpacity(0.1) : null, + shape: BoxShape.circle, + + ), + child: Icon( + _isRecording ? Icons.mic : Icons.mic_none, + color: _isRecording ? Colors.red : null, + ), + ), + ), + IconButton( + icon: const Icon(Icons.send), + onPressed: () => _sendMessage(context), + ), + ], ), ); } @@ -67,7 +210,6 @@ class _MessagePanelState extends State { if (myController.text.isEmpty) { return; } - // Encode the message to base64 String data = jsonEncode({ "sender": Global.myName, @@ -95,7 +237,6 @@ class _MessagePanelState extends State { ); RSAPublicKey publicKey = Global.myPublicKey!; - // Encrypt the message Uint8List encryptedMessage = rsaEncrypt( publicKey, Uint8List.fromList(utf8.encode(myController.text))); @@ -110,21 +251,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 +332,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 +351,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 +365,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 +378,6 @@ class _MessagePanelState extends State { ], ), actions: [ - TextButton( onPressed: () { Navigator.of(context).pop(); @@ -195,10 +386,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 +400,36 @@ class _MessagePanelState extends State { } } + void _sendFileMessage(BuildContext context, File file) async { + -/// This function is used to send the file message. + void _sendVoiceMessage(File audioFile) async { + final String msgId = nanoid(21); + final String fileName = 'voice_${DateTime.now().millisecondsSinceEpoch}.aac'; + + final String data = jsonEncode({ + "sender": Global.myName, + "type": "voice", + "fileName": fileName, + "filePath": audioFile.path, + }); + + final String date = DateTime.now().toUtc().toString(); + final payload = Payload(msgId, Global.myName, widget.converser, data, date); + + Global.cache[msgId] = payload; + insertIntoMessageTable(payload); + + if (!mounted) return; + Provider.of(context, listen: false).sentToConversations( + Msg(data, "sent", date, msgId), + widget.converser, + ); + } + + /// This function is used to send the file message. void _sendFileMessage(BuildContext context, File file) async{ + var msgId = nanoid(21); String fileName = _selectedFile.path.split('/').last; @@ -247,7 +464,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..bac4556 100644 --- a/lib/encyption/rsa.dart +++ b/lib/encyption/rsa.dart @@ -1,19 +1,24 @@ 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"; - -AsymmetricKeyPair generateRSAkeyPair( - SecureRandom secureRandom, - {int bitLength = 2048}) { +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() ..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 +26,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 +94,78 @@ 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); } +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/main.dart b/lib/main.dart index 67ce4fc..72047bd 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,26 +4,93 @@ import 'package:flutter_nearby_connections_example/pages/auth_fail.dart'; import 'package:flutter_nearby_connections_example/pages/profile.dart'; import 'package:local_auth/local_auth.dart'; import 'package:provider/provider.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'classes/global.dart'; import 'encyption/key_storage.dart'; import 'encyption/rsa.dart'; +import 'providers/theme_provider.dart'; + +import 'dart:io'; +import 'package:device_info_plus/device_info_plus.dart'; + +Future requestPermissions() async { + try { + if (Platform.isAndroid) { + final androidInfo = await DeviceInfoPlugin().androidInfo; + final sdkInt = androidInfo.version.sdkInt; + + // Base permissions for all Android versions + final basePermissions = [ + Permission.storage, + Permission.microphone, + Permission.location, + Permission.bluetooth, + ]; + + // Permissions for Android 12 and above + final modernPermissions = [ + Permission.bluetoothScan, + Permission.bluetoothAdvertise, + Permission.bluetoothConnect, + Permission.nearbyWifiDevices, + ]; + + // Permissions for Android 10 and above + final storagePermissions = [ + Permission.manageExternalStorage, + ]; + + final permissions = [...basePermissions]; + + if (sdkInt >= 31) { // Android 12 or higher + permissions.addAll(modernPermissions); + } + + if (sdkInt >= 29) { // Android 10 or higher + permissions.addAll(storagePermissions); + } + + for (var permission in permissions) { + if (await permission.status.isDenied) { + final status = await permission.request(); + if (status.isPermanentlyDenied) { + openAppSettings(); + break; + } + } + } + } else { + // iOS permissions + await Permission.microphone.request(); + await Permission.bluetooth.request(); + await Permission.location.request(); + } + } catch (e) { + debugPrint('Permission request error: $e'); + } +} + + + + + void main() async { WidgetsFlutterBinding.ensureInitialized(); + // Request permissions first + await requestPermissions(); + final keyStorage = KeyStorage(); - // Check if keys already exist String? privateKeyPem = await keyStorage.getPrivateKey(); String? publicKeyPem = await keyStorage.getPublicKey(); if (privateKeyPem == null || publicKeyPem == null) { - // Generate RSA key pair final pair = generateRSAkeyPair(exampleSecureRandom()); privateKeyPem = encodePrivateKeyToPem(pair.privateKey); publicKeyPem = encodePublicKeyToPem(pair.publicKey); - // Store keys await keyStorage.savePrivateKey(privateKeyPem); await keyStorage.savePublicKey(publicKeyPem); } @@ -37,6 +104,7 @@ void main() async { ChangeNotifierProvider( create: (_) => Global(), ), + ChangeNotifierProvider(create: (_) => ThemeProvider()), ], child: const MyApp(), ), @@ -48,15 +116,19 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return const MaterialApp( - debugShowCheckedModeBanner: false, - onGenerateRoute: generateRoute, - initialRoute: '/', + return Consumer( + builder: (context, themeProvider, _) { + return MaterialApp( + theme: themeProvider.theme, + debugShowCheckedModeBanner: false, + onGenerateRoute: generateRoute, + initialRoute: '/', + ); + } ); } } - Future _authenticate(BuildContext context) async { final LocalAuthentication auth = LocalAuthentication(); bool authenticated = false; @@ -100,4 +172,3 @@ Route generateRoute(RouteSettings settings) { }, ); } - diff --git a/lib/p2p/adhoc_housekeeping.dart b/lib/p2p/adhoc_housekeeping.dart index 77b0ffe..1add305 100644 --- a/lib/p2p/adhoc_housekeeping.dart +++ b/lib/p2p/adhoc_housekeeping.dart @@ -144,7 +144,7 @@ void broadcast(BuildContext context) async { "data": encodedMessage, }; } - else if (message['type'] == 'file') { + else if (message['type'] == 'voice' || message['type'] == 'file') { File file = File(message['filePath']); Uint8List encryptedBytes = await file.readAsBytes(); @@ -160,7 +160,7 @@ void broadcast(BuildContext context) async { finalData = { - "type": "file", + "type": message['type'], "data": encodedMessage, "fileName": message['fileName'], }; diff --git a/lib/pages/chat_page.dart b/lib/pages/chat_page.dart index 33a2518..705e005 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'; @@ -11,31 +10,95 @@ import 'dart:convert'; import 'package:pointycastle/asymmetric/api.dart'; import '../components/view_file.dart'; import '../encyption/rsa.dart'; +import 'package:audioplayers/audioplayers.dart'; class ChatPage extends StatefulWidget { - const ChatPage({Key? key, required this.converser}) : super(key: key); - - final String converser; + String converser; + ChatPage({Key? key, required this.converser}) : super(key: key); @override - ChatPageState createState() => ChatPageState(); + _ChatPageState createState() => _ChatPageState(); } -class ChatPageState extends State { +class _ChatPageState extends State { + final ScrollController _scrollController = ScrollController(); List messageList = []; + TextEditingController myController = TextEditingController(); + final AudioPlayer _audioPlayer = AudioPlayer(); + String? _currentlyPlayingId; + bool _isPlaying = false; + final ScrollController _scrollController = ScrollController(); + bool _isFirstBuild = true; // Add this flag + + String _formatDuration(Duration duration) { + String twoDigits(int n) => n.toString().padLeft(2, '0'); + String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60)); + String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60)); + return "$twoDigitMinutes:$twoDigitSeconds"; + } @override void initState() { super.initState(); + + _subscribeToProfileUpdates(); + } + + void _subscribeToProfileUpdates() { + Global.profileNameStream.listen((updatedName) { + if (widget.converser == Global.myName) { + setState(() { + widget.converser = updatedName; + }); + } + }); + } + + void _scrollToBottom() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_scrollController.hasClients) { + _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + _audioPlayer.setReleaseMode(ReleaseMode.stop); // Stop when completed + _audioPlayer.onPlayerComplete.listen((event) { + if (mounted) { + setState(() { + _currentlyPlayingId = null; + _isPlaying = false; + }); + } + }); + WidgetsBinding.instance.addPostFrameCallback((_) { + _scrollToBottom(); + }); } + @override + void dispose() { + _scrollController.dispose(); + _audioPlayer.dispose(); + super.dispose(); + } + + void _scrollToBottom() { + if (_scrollController.hasClients) { + _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + } + } + @override void didChangeDependencies() { super.didChangeDependencies(); } - final ScrollController _scrollController = ScrollController(); @override Widget build(BuildContext context) { @@ -46,137 +109,386 @@ class ChatPageState extends State { .forEach((key, value) { messageList.add(value); }); + if (_isFirstBuild) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _scrollToBottom(); + _isFirstBuild = false; + }); - if (_scrollController.hasClients) { - _scrollController.animateTo( - _scrollController.position.maxScrollExtent + 50, - 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( 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: [ 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) { - 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), + + controller: _scrollController, + padding: const EdgeInsets.all(8), + itemCount: groupedMessages.keys.length, + itemBuilder: (context, index) { + String date = groupedMessages.keys.elementAt(index); + return _buildMessageGroup(date, groupedMessages[date]!); + }, + ), + + 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); + } + } + 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), + ), + ), + ), + 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, + onMessageSent: () { + // Scroll to bottom when new message is sent + WidgetsBinding.instance.addPostFrameCallback((_) { + _scrollToBottom(); + }); + }, + ), + ], + ), + ); + } + Widget _buildVoiceMessageBubble(Msg msg) { + final data = jsonDecode(msg.message); + final bool isCurrentlyPlaying = _currentlyPlayingId == msg.id; + + return Container( + constraints: const BoxConstraints(maxWidth: 280), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + // Play/Pause Button + Container( + height: 40, + width: 40, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: msg.msgtype == 'sent' + ? Colors.deepPurple.withOpacity(0.2) + : Colors.cyan.withOpacity(0.2), + ), + child: IconButton( + padding: EdgeInsets.zero, + icon: Icon( + isCurrentlyPlaying && _isPlaying + ? Icons.pause_rounded + : Icons.play_arrow_rounded, + color: msg.msgtype == 'sent' + ? Colors.deepPurple + : Colors.cyan[700], + size: 24, + ), + onPressed: () => _handleVoicePlayback(msg), + ), + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + // Waveform/Progress Bar + Container( + height: 28, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: StreamBuilder( + stream: _audioPlayer.onPositionChanged, + builder: (context, snapshot) { + return FutureBuilder( + future: _audioPlayer.getDuration(), + builder: (context, durationSnapshot) { + final totalDuration = durationSnapshot.data?.inMilliseconds ?? 1; + return LinearProgressIndicator( + value: isCurrentlyPlaying && snapshot.hasData + ? snapshot.data!.inMilliseconds / totalDuration + : 0, + backgroundColor: msg.msgtype == 'sent' + ? Colors.deepPurple.withOpacity(0.1) + : Colors.cyan.withOpacity(0.1), + valueColor: AlwaysStoppedAnimation( + msg.msgtype == 'sent' + ? Colors.deepPurple + : Colors.cyan[700]!, + ), + ); + }, + ); + }, ), - ...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); + ), + ), + const SizedBox(height: 4), + // Duration Text + StreamBuilder( + stream: _audioPlayer.onPositionChanged, + builder: (context, snapshot) { + return FutureBuilder( + future: _audioPlayer.getDuration(), + builder: (context, durationSnapshot) { + String duration = '0:00'; + if (isCurrentlyPlaying && snapshot.hasData) { + duration = _formatDuration(snapshot.data!); + } else if (durationSnapshot.hasData) { + duration = _formatDuration(durationSnapshot.data!); } - } - 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), - ), - ), + return Text( + duration, + style: TextStyle( + fontSize: 12, + color: msg.msgtype == 'sent' + ? Colors.deepPurple[700] + : Colors.cyan[900], ), - 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), ], ), ); } + 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), + ), + ), + ], + ); + } + + + Future _handleVoicePlayback(Msg msg) async { + try { + final data = jsonDecode(msg.message); + final String filePath = data['filePath']; + + if (_currentlyPlayingId == msg.id && _isPlaying) { + await _audioPlayer.pause(); + setState(() => _isPlaying = false); + } else { + if (_currentlyPlayingId != msg.id) { + await _audioPlayer.stop(); + await _audioPlayer.setSource(DeviceFileSource(filePath)); + await _audioPlayer.resume(); + } else { + await _audioPlayer.resume(); + } + setState(() { + _currentlyPlayingId = msg.id; + _isPlaying = true; + }); + } + } catch (e) { + debugPrint('Error playing audio: $e'); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Error playing voice message')), + ); + } + } + Widget _buildFileBubble(Msg msg) { dynamic data = jsonDecode(msg.message); + if (data['type'] == 'voice') { + return _buildVoiceMessageBubble(msg); + } String fileName = data['fileName']; String filePath = data['filePath']; + return Row( mainAxisSize: MainAxisSize.min, children: [ 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/lib/pages/device_list_screen.dart b/lib/pages/device_list_screen.dart index a1a5f92..0d8af39 100644 --- a/lib/pages/device_list_screen.dart +++ b/lib/pages/device_list_screen.dart @@ -26,45 +26,62 @@ class DevicesListScreen extends StatefulWidget { class _DevicesListScreenState extends State { bool isInit = false; - bool isLoading = false; - TextEditingController searchController = TextEditingController(); List filteredDevices = []; - + Global? globalProvider; @override - void initState() { - super.initState(); - searchController.addListener(_filterDevices); + void didChangeDependencies() { + super.didChangeDependencies(); + if (!isInit) { + globalProvider = Provider.of(context, listen: false); + globalProvider?.addListener(_handleGlobalUpdate); + _updateFilteredDevices(); + isInit = true; + } + } + + void _handleGlobalUpdate() { + if (mounted) { + _updateFilteredDevices(); + } + } + + void _updateFilteredDevices() { + if (mounted) { + setState(() { + if (searchController.text.isEmpty) { + filteredDevices = globalProvider?.devices ?? []; + } else { + _filterDevices(); + } + }); + } } @override void dispose() { searchController.removeListener(_filterDevices); + globalProvider?.removeListener(_handleGlobalUpdate); searchController.dispose(); super.dispose(); } void _filterDevices() { - setState(() { - if (searchController.text.isEmpty) { - filteredDevices = Provider.of(context, listen: false).devices; - } else { - filteredDevices = Provider.of(context, listen: false) - .devices - .where((device) => device.deviceName.toLowerCase().contains(searchController.text.toLowerCase())) + if (mounted && globalProvider != null) { + setState(() { + filteredDevices = globalProvider!.devices + .where((device) => device.deviceName + .toLowerCase() + .contains(searchController.text.toLowerCase())) .toList(); - } - }); + }); + } } - @override Widget build(BuildContext context) { - if (filteredDevices.isEmpty && searchController.text.isEmpty) { - filteredDevices = Provider.of(context).devices; - } return SingleChildScrollView( child: Column( children: [ @@ -91,11 +108,11 @@ class _DevicesListScreenState extends State { ), ListView.builder( // Builds a screen with list of devices in the proximity - itemCount: filteredDevices.length, + itemCount: filteredDevices.length, shrinkWrap: true, itemBuilder: (context, index) { - // Getting a device from the provider - final device = Provider.of(context).devices[index]; + // Get device from filteredDevices + final device = filteredDevices[index]; return Container( margin: const EdgeInsets.all(8.0), child: Column( @@ -120,7 +137,8 @@ class _DevicesListScreenState extends State { getButtonStateName(device.state), style: const TextStyle( color: Colors.white, - fontWeight: FontWeight.bold), + fontWeight: FontWeight.bold + ), ), ), ), @@ -130,19 +148,14 @@ class _DevicesListScreenState extends State { // ChatPage. Navigator.of(context).push( MaterialPageRoute( - builder: (context) { - return ChatPage( - converser: device.deviceName, - ); - }, + builder: (context) => ChatPage( + converser: device.deviceName, + ), ), ); }, ), - const Divider( - height: 1, - color: Colors.grey, - ), + const Divider(height: 1, color: Colors.grey), ], ), ); diff --git a/lib/pages/profile.dart b/lib/pages/profile.dart index 6e344e7..a3b2480 100644 --- a/lib/pages/profile.dart +++ b/lib/pages/profile.dart @@ -1,130 +1,492 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'home_screen.dart'; -import 'package:nanoid/nanoid.dart'; +import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:nanoid/nanoid.dart'; import '../classes/global.dart'; +import feature/chat-history-export +import '../services/communication_service.dart'; +import '../providers/theme_provider.dart'; +import 'home_screen.dart'; + main class Profile extends StatefulWidget { final bool onLogin; - const Profile({Key? key, required this.onLogin}) : super(key: key); + @override State createState() => _ProfileState(); } -class _ProfileState extends State { - // TextEditingController for the name of the user - TextEditingController myName = TextEditingController(); - - // loading variable is used for UI purpose when the app is fetching - // user details - bool loading = true; - - // Custom generated id for the user - var customLengthId = nanoid(6); - - // Fetching details from saved profile - // If no profile is saved, then the new values are used - // else navigate to DeviceListScreen - Future getDetails() async { - // Obtain shared preferences. - final prefs = await SharedPreferences.getInstance(); - final name = prefs.getString('p_name') ?? ''; - final id = prefs.getString('p_id') ?? ''; - setState(() { - myName.text = name; - customLengthId = id.isNotEmpty ? id : customLengthId; - }); - if (name.isNotEmpty && id.isNotEmpty && widget.onLogin) { - navigateToHomeScreen(); - } else { - setState(() { - loading = false; - }); +class _ProfileState extends State with SingleTickerProviderStateMixin { + final _formKey = GlobalKey(); + final _nameController = TextEditingController(); + late final AnimationController _animationController; + late final Animation _fadeAnimation; + + String _userId = ''; + bool _isLoading = true; + bool _isSaving = false; + + @override + void initState() { + super.initState(); + _setupAnimations(); + _loadProfileData(); + } + + void _setupAnimations() { + _animationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 800), + ); + _fadeAnimation = CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOut, + ); + _animationController.forward(); + } + + Future _loadProfileData() async { + try { + final prefs = await SharedPreferences.getInstance(); + final name = prefs.getString('p_name') ?? ''; + final savedId = prefs.getString('p_id') ?? ''; + + if (mounted) { + setState(() { + _nameController.text = name; + _userId = savedId.isNotEmpty ? savedId : nanoid(6); + _isLoading = false; + }); + + if (name.isNotEmpty && savedId.isNotEmpty && widget.onLogin) { + Global.myName = name; // Ensure name is set before navigation + _navigateToHome(); + } + } + } catch (e) { + _handleError('Failed to load profile', e); + } + } + + Future _saveProfile() async { + if (!_formKey.currentState!.validate()) return; + + setState(() => _isSaving = true); + try { + final prefs = await SharedPreferences.getInstance(); + final name = _nameController.text.trim(); + + await prefs.setString('p_name', name); + await prefs.setString('p_id', _userId); + + Global.myName = name; // Set global name before navigation + _navigateToHome(); + } catch (e) { + _handleError('Failed to save profile', e); + } finally { + if (mounted) setState(() => _isSaving = false); } } - // It is a general function to navigate to home screen. - // If we are first launching the app, we need to replace the profile page - // from the context and then open the home screen - // Otherwise we need to pop out the profile screen context - // from memory of the application. This is a flutter way - // to manage different contexts and screens. - void navigateToHomeScreen() { - Global.myName = myName.text; + + void _navigateToHome() { if (!widget.onLogin) { - Global.myName = myName.text; Navigator.pop(context); } else { Navigator.pushReplacement( context, - MaterialPageRoute( - builder: (context) => const HomeScreen(), - ), + MaterialPageRoute(builder: (_) => const HomeScreen()), ); } } - @override - void initState() { - super.initState(); - - // At the launch we are fetching details using the getDetails function - getDetails(); + void _handleError(String message, dynamic error) { + if (!mounted) return; + setState(() => _isLoading = false); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('$message: $error'), + behavior: SnackBarBehavior.floating, + ), + ); } - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text( - 'Profile', - ), - ), - body: Visibility( - visible: loading, - replacement: Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ + Widget _buildProfileCard() { + final colorScheme = Theme.of(context).colorScheme; - Padding( - padding: const EdgeInsets.all(10.0), - child: TextFormField( - controller: myName, - decoration: const InputDecoration( - icon: Icon(Icons.person), - hintText: 'What do people call you?', - labelText: 'Name *', - border: OutlineInputBorder(), + return Card( + elevation: 8, + shadowColor: colorScheme.shadow.withOpacity(0.2), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: Padding( + padding: const EdgeInsets.all(24), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Profile Details', + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + color: colorScheme.primary, ), - validator: (String? value) { - return (value != null && - value.contains('@') && - value.length > 3) - ? 'Do not use the @ char and name length should be greater than 3' - : null; - }, ), + ), + feature/chat-history-export ElevatedButton( onPressed: () async { final prefs = await SharedPreferences.getInstance(); // 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(); }, child: const Text("Save"), ) + + Container( + height: 120, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: ThemeProvider.availableThemes.length, + itemBuilder: (context, index) { + String themeName = ThemeProvider.availableThemes.keys.elementAt(index); + bool isSelected = themeName == themeProvider.currentTheme; + + return Padding( + padding: const EdgeInsets.all(8.0), + child: Material( + elevation: isSelected ? 8 : 2, + borderRadius: BorderRadius.circular(12), + child: InkWell( + onTap: () => themeProvider.setTheme(themeName), + borderRadius: BorderRadius.circular(12), + child: Container( + width: 100, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: isSelected + ? Border.all( + color: Theme.of(context).colorScheme.primary, + width: 2, + ) + : null, + + const SizedBox(height: 24), + TextFormField( + controller: _nameController, + validator: (value) => + value!.trim().isEmpty ? 'Name is required' : null, + decoration: InputDecoration( + labelText: 'Display Name', + hintText: 'Enter your display name', + prefixIcon: Icon(Icons.person_outline, + color: colorScheme.primary), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + ), + ), + ), + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Icon(Icons.fingerprint, color: colorScheme.primary), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Your Unique ID', + style: TextStyle( + color: colorScheme.onSurfaceVariant, + + ), + ), + Text( + _userId, + style: TextStyle( + fontWeight: FontWeight.bold, + color: colorScheme.primary, + ), + ), + ], + ), + ], + ), + ), + + ), + ], + ); + }, + + ], + ), ), - child: const Center( + ), + + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Profile Setup'), + centerTitle: true, + ), + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : FadeTransition( + opacity: _fadeAnimation, + child: CustomScrollView( + slivers: [ + SliverFillRemaining( + hasScrollBody: false, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + _buildProfileCard(), + const SizedBox(height: 20), + const ThemeSelector(), + // Minimum spacing that ensures button visibility + SizedBox(height: MediaQuery.of(context).size.height * 0.08), + _buildSaveButton(), + // Safe area padding for bottom + SizedBox(height: MediaQuery.of(context).padding.bottom + 16), + ], + ), + ), + ), + ], + ), + ), + ); + } + + + Widget _buildSaveButton() { + return SizedBox( + width: double.infinity, + height: 56, + child: ElevatedButton( + onPressed: _isSaving ? null : _saveProfile, + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + child: _isSaving + ? const SizedBox( + height: 24, + width: 24, child: CircularProgressIndicator(), + ) + : const Text( + 'Save Profile', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), ), ); } + + @override + void dispose() { + _nameController.dispose(); + _animationController.dispose(); + super.dispose(); + } +} +class ThemeSelector extends StatelessWidget { + const ThemeSelector({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Padding( + padding: EdgeInsets.all(16.0), + child: Text( + 'Choose Base Theme', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ), + BaseThemeSelector(), + + const SizedBox(height: 24), + + const Padding( + padding: EdgeInsets.all(16.0), + child: Text( + 'Choose Color Scheme (Optional)', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ), + ColorSchemeSelector(), + ], + ); + } +} + +class BaseThemeSelector extends StatelessWidget { + const BaseThemeSelector({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final themeProvider = Provider.of(context); + + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: ThemeProvider.baseThemes.keys.map((themeName) { + final isSelected = themeName == themeProvider.baseTheme; + + return Padding( + padding: const EdgeInsets.only(right: 12), + child: ThemeOption( + name: themeName, + isSelected: isSelected, + onTap: () => themeProvider.setBaseTheme(themeName), + color: themeName == 'Light' + ? Colors.blue.shade100 + : Colors.grey.shade800, + secondaryColor: themeName == 'Light' + ? Colors.blue.shade200 + : Colors.grey.shade900, + ), + + ); + }).toList(), + ), + ); + } +} + +class ColorSchemeSelector extends StatelessWidget { + const ColorSchemeSelector({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final themeProvider = Provider.of(context); + final isDark = Theme.of(context).brightness == Brightness.dark; + + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: ThemeProvider.colorSchemes.entries.map((entry) { + final isSelected = entry.key == themeProvider.colorSchemeName; + final scheme = isDark + ? ThemeProvider.getDarkScheme(entry.key) + : entry.value; + + return Padding( + padding: const EdgeInsets.only(right: 12), + child: ThemeOption( + name: entry.key, + isSelected: isSelected, + onTap: () => themeProvider.setColorScheme(entry.key), + color: scheme.primary, + secondaryColor: scheme.secondary, + ), + ); + }).toList(), + ), + ); + } +} + +class ThemeOption extends StatelessWidget { + final String name; + final bool isSelected; + final VoidCallback onTap; + final Color color; + final Color secondaryColor; + + const ThemeOption({ + Key? key, + required this.name, + required this.isSelected, + required this.onTap, + required this.color, + required this.secondaryColor, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme + .of(context) + .colorScheme; + + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [color, secondaryColor], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isSelected ? colorScheme.primary : Colors.transparent, + width: 2, + ), + boxShadow: isSelected ? [ + BoxShadow( + color: colorScheme.shadow.withOpacity(0.2), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ] : null, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + name, + style: TextStyle( + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + color: color.computeLuminance() > 0.5 ? Colors.black : Colors + .white, + ), + ), + if (isSelected) + Icon( + Icons.check_circle, + size: 16, + color: color.computeLuminance() > 0.5 ? Colors.black : Colors + .white, + ), + ], + ), + ), + ); + } + } \ No newline at end of file diff --git a/lib/providers/theme_colors.dart b/lib/providers/theme_colors.dart new file mode 100644 index 0000000..ca285d8 --- /dev/null +++ b/lib/providers/theme_colors.dart @@ -0,0 +1,303 @@ +import 'package:flutter/material.dart'; + +class ThemeColors { + // Extended color properties for all schemes + + // Light Theme Colors + static final lightColorScheme = ColorScheme.light( + primary: const Color(0xFF0061A4), + onPrimary: const Color(0xFFFFFFFF), + primaryContainer: const Color(0xFFD1E4FF), + onPrimaryContainer: const Color(0xFF001D36), + secondary: const Color(0xFF535F70), + onSecondary: const Color(0xFFFFFFFF), + secondaryContainer: const Color(0xFFD7E3F7), + onSecondaryContainer: const Color(0xFF101C2B), + surface: const Color(0xFFFBFCFF), + onSurface: const Color(0xFF1A1C1E), + surfaceContainerHighest: const Color(0xFFDFE2EB), + onSurfaceVariant: const Color(0xFF43474E), + // Add error colors for form validation + error: const Color(0xFFBA1A1A), + onError: const Color(0xFFFFFFFF), + errorContainer: const Color(0xFFFFDAD6), + onErrorContainer: const Color(0xFF410002), + // Add shadow color for elevation + shadow: Colors.black.withOpacity(0.1), + ); + + // Dark Theme Colors + static final darkColorScheme = ColorScheme.dark( + primary: const Color(0xFF9ECAFF), + onPrimary: const Color(0xFF003258), + primaryContainer: const Color(0xFF00497D), + onPrimaryContainer: const Color(0xFFD1E4FF), + secondary: const Color(0xFFBBC7DB), + onSecondary: const Color(0xFF253140), + secondaryContainer: const Color(0xFF3B4858), + onSecondaryContainer: const Color(0xFFD7E3F7), + surface: const Color(0xFF1A1C1E), + onSurface: const Color(0xFFE2E2E6), + surfaceContainerHighest: const Color(0xFF43474E), + onSurfaceVariant: const Color(0xFFC3C7CF), + // Add error colors for form validation + error: const Color(0xFFFFB4AB), + onError: const Color(0xFF690005), + errorContainer: const Color(0xFF93000A), + onErrorContainer: const Color(0xFFFFDAD6), + // Add shadow color for elevation + shadow: Colors.black.withOpacity(0.3), + ); + + // Nature Theme Colors + static final natureColorScheme = ColorScheme.light( + primary: const Color(0xFF246C2C), + onPrimary: const Color(0xFFFFFFFF), + primaryContainer: const Color(0xFFA8F5A4), + onPrimaryContainer: const Color(0xFF002204), + secondary: const Color(0xFF52634F), + onSecondary: const Color(0xFFFFFFFF), + secondaryContainer: const Color(0xFFD5E8CF), + onSecondaryContainer: const Color(0xFF101F0F), + surface: const Color(0xFFFBFDF7), + onSurface: const Color(0xFF1A1C19), + surfaceContainerHighest: const Color(0xFFDEE5D9), + onSurfaceVariant: const Color(0xFF424940), + error: const Color(0xFFBA1A1A), + onError: const Color(0xFFFFFFFF), + errorContainer: const Color(0xFFFFDAD6), + onErrorContainer: const Color(0xFF410002), + shadow: Colors.black.withOpacity(0.1), + ); + + static final natureDarkColorScheme = ColorScheme.dark( + primary: const Color(0xFF8CD889), + onPrimary: const Color(0xFF003909), + primaryContainer: const Color(0xFF165219), + onPrimaryContainer: const Color(0xFFA8F5A4), + secondary: const Color(0xFFB9CCB4), + onSecondary: const Color(0xFF263423), + secondaryContainer: const Color(0xFF3C4B38), + onSecondaryContainer: const Color(0xFFD5E8CF), + surface: const Color(0xFF1A1C19), + onSurface: const Color(0xFFE2E3DE), + surfaceContainerHighest: const Color(0xFF424940), + onSurfaceVariant: const Color(0xFFC2C9BE), + error: const Color(0xFFFFB4AB), + onError: const Color(0xFF690005), + errorContainer: const Color(0xFF93000A), + onErrorContainer: const Color(0xFFFFDAD6), + shadow: Colors.black.withOpacity(0.3), + ); + + // Amber Theme Colors + static final amberColorScheme = ColorScheme.light( + primary: const Color(0xFFFFA726), + onPrimary: const Color(0xFF000000), + primaryContainer: const Color(0xFFFFECB3), + onPrimaryContainer: const Color(0xFF261900), + secondary: const Color(0xFFFFB74D), + onSecondary: const Color(0xFF000000), + secondaryContainer: const Color(0xFFFFF3E0), + onSecondaryContainer: const Color(0xFF261900), + surface: const Color(0xFFFFFBF5), + onSurface: const Color(0xFF1A1A1A), + surfaceContainerHighest: const Color(0xFFFFEFD5), + onSurfaceVariant: const Color(0xFF4E4B40), + error: const Color(0xFFBA1A1A), + onError: const Color(0xFFFFFFFF), + errorContainer: const Color(0xFFFFDAD6), + onErrorContainer: const Color(0xFF410002), + shadow: Colors.black.withOpacity(0.1), + ); + + static final amberDarkColorScheme = ColorScheme.dark( + primary: const Color(0xFFFFD180), + onPrimary: const Color(0xFF2B1700), + primaryContainer: const Color(0xFFFF9800), + onPrimaryContainer: const Color(0xFFFFECCC), + secondary: const Color(0xFFFFE0B2), + onSecondary: const Color(0xFF261500), + secondaryContainer: const Color(0xFFE65100), + onSecondaryContainer: const Color(0xFFFFF4E6), + surface: const Color(0xFF121212), + onSurface: const Color(0xFFFAFAFA), + surfaceContainerHighest: const Color(0xFF3D3833), + onSurfaceVariant: const Color(0xFFE8DED2), + error: const Color(0xFFFF8A80), + onError: const Color(0xFF480000), + errorContainer: const Color(0xFFB71C1C), + onErrorContainer: const Color(0xFFFFEBEE), + shadow: Colors.black.withOpacity(0.4), + ); + + + + // Rose Theme Colors + static final roseColorScheme = ColorScheme.light( + primary: const Color(0xFFE84A5F), + onPrimary: const Color(0xFFFFFFFF), + primaryContainer: const Color(0xFFFFE4E8), + onPrimaryContainer: const Color(0xFF400012), + secondary: const Color(0xFF9D8189), + onSecondary: const Color(0xFFFFFFFF), + secondaryContainer: const Color(0xFFFFD8E4), + onSecondaryContainer: const Color(0xFF2E1519), + surface: const Color(0xFFFFF5F7), + onSurface: const Color(0xFF1A1A1A), + surfaceContainerHighest: const Color(0xFFFFECF1), + onSurfaceVariant: const Color(0xFF534346), + error: const Color(0xFFBA1A1A), + onError: const Color(0xFFFFFFFF), + errorContainer: const Color(0xFFFFDAD6), + onErrorContainer: const Color(0xFF410002), + shadow: Colors.black.withOpacity(0.1), + ); + + static final roseDarkColorScheme = ColorScheme.dark( + primary: const Color(0xFFFF8FA3), + onPrimary: const Color(0xFF4A0012), + primaryContainer: const Color(0xFFB4364A), + onPrimaryContainer: const Color(0xFFFFE4E8), + secondary: const Color(0xFFD1A0AA), + onSecondary: const Color(0xFF2B1419), + secondaryContainer: const Color(0xFF432931), + onSecondaryContainer: const Color(0xFFFFD8E4), + surface: const Color(0xFF151111), + onSurface: const Color(0xFFE8E0E1), + surfaceContainerHighest: const Color(0xFF3D2F32), + onSurfaceVariant: const Color(0xFFD6C2C7), + error: const Color(0xFFFFB4AB), + onError: const Color(0xFF690005), + errorContainer: const Color(0xFF93000A), + onErrorContainer: const Color(0xFFFFDAD6), + shadow: Colors.black.withOpacity(0.3), + ); + + + // Bubblegum Theme Colors + static final bubblegumColorScheme = ColorScheme.light( + primary: const Color(0xFFFF69B4), + onPrimary: const Color(0xFFFFFFFF), + primaryContainer: const Color(0xFFFFD6E9), + onPrimaryContainer: const Color(0xFF3F0020), + secondary: const Color(0xFFFF9ECD), + onSecondary: const Color(0xFF000000), + secondaryContainer: const Color(0xFFFFE3F1), + onSecondaryContainer: const Color(0xFF3F002D), + surface: const Color(0xFFFFF5F9), + onSurface: const Color(0xFF1A1A1A), + surfaceContainerHighest: const Color(0xFFFFE6F3), + onSurfaceVariant: const Color(0xFF534347), + error: const Color(0xFFBA1A1A), + onError: const Color(0xFFFFFFFF), + errorContainer: const Color(0xFFFFDAD6), + onErrorContainer: const Color(0xFF410002), + shadow: Colors.black.withOpacity(0.1), + ); + + static final bubblegumDarkColorScheme = ColorScheme.dark( + primary: const Color(0xFFFF80CE), + onPrimary: const Color(0xFF3B0026), + primaryContainer: const Color(0xFFD4458B), + onPrimaryContainer: const Color(0xFFFFD6E9), + secondary: const Color(0xFFFF9ED2), + onSecondary: const Color(0xFF330024), + secondaryContainer: const Color(0xFF8B2E63), + onSecondaryContainer: const Color(0xFFFFE3F1), + surface: const Color(0xFF120D0F), + onSurface: const Color(0xFFE8E0E4), + surfaceContainerHighest: const Color(0xFF3D2934), + onSurfaceVariant: const Color(0xFFD6C2C8), + error: const Color(0xFFFFB4AB), + onError: const Color(0xFF690005), + errorContainer: const Color(0xFF93000A), + onErrorContainer: const Color(0xFFFFDAD6), + shadow: Colors.black.withOpacity(0.3), + ); + + // Lavender Theme + static final lavenderColorScheme = ColorScheme.light( + primary: const Color(0xFF9575CD), + onPrimary: const Color(0xFFFFFFFF), + primaryContainer: const Color(0xFFEDE7F6), + onPrimaryContainer: const Color(0xFF1B0057), + secondary: const Color(0xFFB39DDB), + onSecondary: const Color(0xFF000000), + secondaryContainer: const Color(0xFFF3E5F5), + onSecondaryContainer: const Color(0xFF2A0049), + surface: const Color(0xFFFCF8FF), + onSurface: const Color(0xFF1A191C), + surfaceContainerHighest: const Color(0xFFE8E0F0), + onSurfaceVariant: const Color(0xFF49454E), + error: const Color(0xFFBA1A1A), + onError: const Color(0xFFFFFFFF), + errorContainer: const Color(0xFFFFDAD6), + onErrorContainer: const Color(0xFF410002), + shadow: Colors.black.withOpacity(0.1), + ); + + static final lavenderDarkColorScheme = ColorScheme.dark( + primary: const Color(0xFFB39DDB), + onPrimary: const Color(0xFF2A004C), + primaryContainer: const Color(0xFF6A3AA7), + onPrimaryContainer: const Color(0xFFEDE7F6), + secondary: const Color(0xFFD1C4E9), + onSecondary: const Color(0xFF1D0033), + secondaryContainer: const Color(0xFF4527A0), + onSecondaryContainer: const Color(0xFFF3E5F5), + surface: const Color(0xFF120F17), + onSurface: const Color(0xFFE6E1E6), + surfaceContainerHighest: const Color(0xFF332D3F), + onSurfaceVariant: const Color(0xFFCBC4CE), + error: const Color(0xFFFFB4AB), + onError: const Color(0xFF690005), + errorContainer: const Color(0xFF93000A), + onErrorContainer: const Color(0xFFFFDAD6), + shadow: Colors.black.withOpacity(0.3), + ); + +// Neon Theme + static final neonColorScheme = ColorScheme.light( + primary: const Color(0xFF00CC7D), + onPrimary: const Color(0xFF000000), + primaryContainer: const Color(0xFF80FFB9), + onPrimaryContainer: const Color(0xFF002117), + secondary: const Color(0xFF00B8C4), + onSecondary: const Color(0xFF000000), + secondaryContainer: const Color(0xFF80F4FF), + onSecondaryContainer: const Color(0xFF002022), + surface: const Color(0xFFECF4F2), + onSurface: const Color(0xFF121212), + surfaceContainerHighest: const Color(0xFFD8E8E4), + onSurfaceVariant: const Color(0xFF1F2625), + error: const Color(0xFFE60052), + onError: const Color(0xFFFFFFFF), + errorContainer: const Color(0xFFFFB3CB), + onErrorContainer: const Color(0xFF400016), + shadow: Colors.black.withOpacity(0.2), + ); + + + + static final neonDarkColorScheme = ColorScheme.dark( + primary: const Color(0xFF00FF9C), + onPrimary: const Color(0xFF001A12), + primaryContainer: const Color(0xFF00995D), + onPrimaryContainer: const Color(0xFFB3FFD6), + secondary: const Color(0xFF00F3FF), + onSecondary: const Color(0xFF001618), + secondaryContainer: const Color(0xFF009199), + onSecondaryContainer: const Color(0xFFB3FCFF), + surface: const Color(0xFF050505), + onSurface: const Color(0xFFE0E0E0), + surfaceContainerHighest: const Color(0xFF1A1A1A), + onSurfaceVariant: const Color(0xFFCACACA), + error: const Color(0xFFFF0059), + onError: const Color(0xFFFFFFFF), + errorContainer: const Color(0xFF99003D), + onErrorContainer: const Color(0xFFFFB3CB), + shadow: Colors.black.withOpacity(0.4), + ); + +} diff --git a/lib/providers/theme_components.dart b/lib/providers/theme_components.dart new file mode 100644 index 0000000..35d6ea1 --- /dev/null +++ b/lib/providers/theme_components.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class ThemeComponents { + // Common border radius values + static const double _inputBorderRadius = 12.0; + static const double _buttonBorderRadius = 12.0; + static const double _cardBorderRadius = 16.0; + + // Common padding values + static const EdgeInsets _inputPadding = EdgeInsets.symmetric(horizontal: 16, vertical: 12); + static const EdgeInsets _buttonPadding = EdgeInsets.symmetric(horizontal: 24, vertical: 12); + + static InputDecorationTheme inputDecorationTheme(ColorScheme colors) { + return InputDecorationTheme( + filled: true, + fillColor: colors.surfaceContainerHighest.withOpacity(0.5), + contentPadding: _inputPadding, + border: _buildInputBorder(_inputBorderRadius), + enabledBorder: _buildInputBorder( + _inputBorderRadius, + borderColor: colors.outline.withOpacity(0.3), + ), + focusedBorder: _buildInputBorder( + _inputBorderRadius, + borderColor: colors.primary, + width: 2, + ), + errorBorder: _buildInputBorder( + _inputBorderRadius, + borderColor: colors.error, + ), + // Add focused error border for consistency + focusedErrorBorder: _buildInputBorder( + _inputBorderRadius, + borderColor: colors.error, + width: 2, + ), + ); + } + + static ElevatedButtonThemeData elevatedButtonTheme(ColorScheme colors) { + return ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + elevation: 0, + padding: _buttonPadding, + backgroundColor: colors.primary, + foregroundColor: colors.onPrimary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(_buttonBorderRadius), + ), + // Add disabled colors for better accessibility + disabledBackgroundColor: colors.onSurface.withOpacity(0.12), + disabledForegroundColor: colors.onSurface.withOpacity(0.38), + ), + ); + } + + static CardTheme cardTheme(ColorScheme colors) { + return CardTheme( + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(_cardBorderRadius), + ), + color: colors.surface, + clipBehavior: Clip.antiAlias, + // Add shadow color for better dark mode appearance + shadowColor: colors.shadow, + ); + } + + static AppBarTheme appBarTheme(ColorScheme colors) { + return AppBarTheme( + elevation: 0, + centerTitle: true, + backgroundColor: colors.surface, + foregroundColor: colors.onSurface, + iconTheme: IconThemeData(color: colors.onSurface), + titleTextStyle: TextStyle( + color: colors.onSurface, + fontSize: 20, + fontWeight: FontWeight.w600, + ), + // Add system overlay style for better status bar visibility + systemOverlayStyle: colors.brightness == Brightness.dark + ? SystemUiOverlayStyle.light + : SystemUiOverlayStyle.dark, + ); + } + + // Helper method to build input borders + static OutlineInputBorder _buildInputBorder( + double radius, { + Color borderColor = Colors.transparent, + double width = 1, + }) { + return OutlineInputBorder( + borderRadius: BorderRadius.circular(radius), + borderSide: BorderSide(color: borderColor, width: width), + ); + } +} \ No newline at end of file diff --git a/lib/providers/theme_provider.dart b/lib/providers/theme_provider.dart new file mode 100644 index 0000000..bb1ad99 --- /dev/null +++ b/lib/providers/theme_provider.dart @@ -0,0 +1,147 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'theme_colors.dart'; +import 'theme_components.dart'; + +class ThemeProvider with ChangeNotifier { + static const String baseThemeKey = 'base_theme'; + static const String colorSchemeKey = 'color_scheme'; + + // Base themes (Light/Dark) + static final Map baseThemes = { + 'Light': buildTheme(ThemeColors.lightColorScheme), + 'Dark': buildTheme(ThemeColors.darkColorScheme), + }; + + // Color schemes that can be applied to either base theme + static final Map colorSchemes = { + 'Default': ThemeColors.lightColorScheme, + 'Neon': ThemeColors.neonColorScheme, + 'Amber': ThemeColors.amberColorScheme, + 'Bubblegum': ThemeColors.bubblegumColorScheme, + 'Lavender': ThemeColors.lavenderColorScheme, + 'Rose': ThemeColors.roseColorScheme, + 'Nature': ThemeColors.natureColorScheme, + }; + + + + String _baseTheme = 'Light'; + String _colorScheme = 'Default'; + + ThemeProvider() { + loadTheme(); + } + + String get baseTheme => _baseTheme; + String get colorSchemeName => _colorScheme; + + // Get the current theme data + // Get the current theme data + ThemeData get theme { + if (_colorScheme == 'Default') { + // Use base theme directly + return baseThemes[_baseTheme]!; + } else { + // Get the appropriate color scheme based on base theme + ColorScheme customScheme = _baseTheme == 'Dark' + ? getDarkScheme(_colorScheme) + : colorSchemes[_colorScheme]!; + + return buildTheme(customScheme); + } + } + + + + static ThemeData buildTheme(ColorScheme colorScheme) { + return ThemeData( + useMaterial3: true, + colorScheme: colorScheme, + brightness: colorScheme.brightness, + // Component Themes + inputDecorationTheme: ThemeComponents.inputDecorationTheme(colorScheme), + elevatedButtonTheme: ThemeComponents.elevatedButtonTheme(colorScheme), + cardTheme: ThemeComponents.cardTheme(colorScheme), + appBarTheme: ThemeComponents.appBarTheme(colorScheme), + // Dialog Theme + dialogTheme: DialogTheme( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + backgroundColor: colorScheme.surface, + elevation: 3, + ), + // Bottom Sheet Theme + bottomSheetTheme: BottomSheetThemeData( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + backgroundColor: colorScheme.surface, + ), + // List Tile Theme + listTileTheme: ListTileThemeData( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + tileColor: colorScheme.surface, + iconColor: colorScheme.primary, + ), + // Floating Action Button Theme + floatingActionButtonTheme: FloatingActionButtonThemeData( + backgroundColor: colorScheme.primaryContainer, + foregroundColor: colorScheme.onPrimaryContainer, + elevation: 4, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + ), + // Snackbar Theme + snackBarTheme: SnackBarThemeData( + behavior: SnackBarBehavior.floating, + backgroundColor: colorScheme.inverseSurface, + contentTextStyle: TextStyle(color: colorScheme.onInverseSurface), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + ); + } + + Future loadTheme() async { + final prefs = await SharedPreferences.getInstance(); + _baseTheme = prefs.getString(baseThemeKey) ?? 'Light'; + _colorScheme = prefs.getString(colorSchemeKey) ?? 'Default'; + notifyListeners(); + } + + Future setBaseTheme(String themeName) async { + if (!baseThemes.containsKey(themeName)) return; + + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(baseThemeKey, themeName); + _baseTheme = themeName; + notifyListeners(); + } + + Future setColorScheme(String schemeName) async { + if (!colorSchemes.containsKey(schemeName)) return; + + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(colorSchemeKey, schemeName); + _colorScheme = schemeName; + notifyListeners(); + } + static ColorScheme getDarkScheme(String name) { + switch (name) { + case 'Nature': + return ThemeColors.natureDarkColorScheme; + case 'Amber': + return ThemeColors.amberDarkColorScheme; + case 'Rose': + return ThemeColors.roseDarkColorScheme; + case 'Bubblegum': + return ThemeColors.bubblegumDarkColorScheme; + case 'Lavender': + return ThemeColors.lavenderDarkColorScheme; + case 'Neon': + return ThemeColors.neonDarkColorScheme; + default: + return ThemeColors.darkColorScheme; + } + } + +} \ No newline at end of file diff --git a/lib/services/communication_service.dart b/lib/services/communication_service.dart new file mode 100644 index 0000000..3681a17 --- /dev/null +++ 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/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index d0e7f79..81e6606 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,10 +6,18 @@ #include "generated_plugin_registrant.h" +#include #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) flutter_secure_storage_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_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/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index b29e9ba..db399c1 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,7 +3,9 @@ # list(APPEND FLUTTER_PLUGIN_LIST + audioplayers_linux flutter_secure_storage_linux + record_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 14cd431..cfc77e8 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,16 +5,30 @@ import FlutterMacOS import Foundation +import audio_session + +import audioplayers_darwin +import device_info_plus + import flutter_secure_storage_macos +import just_audio import local_auth_darwin import path_provider_foundation +import record_macos import shared_preferences_foundation -import sqflite +import sqflite_darwin func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) + + AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) + DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) + 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")) + RecordMacosPlugin.register(with: registry.registrar(forPlugin: "RecordMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) } diff --git a/pubspec.yaml b/pubspec.yaml index b3040f7..be88ac6 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 @@ -31,12 +30,26 @@ dependencies: file_picker: ^8.1.2 open_filex: ^4.5.0 permission_handler: ^11.3.1 - path_provider: ^2.1.4 + path_provider: ^2.1.4 web_socket_channel: ^2.2.0 + nanoid: ^1.0.0 + flutter_sound: any + just_audio: ^0.9.14 + pdf: ^3.11.1 + + just_audio: ^0.9.42 + record: ^4.4.4 + audioplayers: ^6.1.0 + device_info_plus: ^11.2.0 + + dev_dependencies: flutter_lints: flutter_test: sdk: flutter + + + flutter: uses-material-design: true \ No newline at end of file diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 16383da..5722166 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,15 +6,21 @@ #include "generated_plugin_registrant.h" +#include #include #include #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + AudioplayersWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin")); FlutterSecureStorageWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); LocalAuthPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("LocalAuthPlugin")); PermissionHandlerWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); + RecordWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("RecordWindowsPluginCApi")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 7badb8c..7de9747 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,9 +3,11 @@ # list(APPEND FLUTTER_PLUGIN_LIST + audioplayers_windows flutter_secure_storage_windows local_auth_windows permission_handler_windows + record_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST