From 8c0ef710eb59c9068ab68253050142c83057ee62 Mon Sep 17 00:00:00 2001 From: khushi-hura Date: Fri, 27 Dec 2024 01:52:15 +0530 Subject: [PATCH 1/7] Fixes issue #28 (Customizable App Themes): On launching the app is set to deafult system them and I have added a toggle button in the home screen on pressing which the theme of the app changes. --- android/app/build.gradle | 2 +- lib/main.dart | 40 ++++- lib/pages/home_screen.dart | 145 ++++++++++++++---- macos/Flutter/GeneratedPluginRegistrant.swift | 2 +- 4 files changed, 153 insertions(+), 36 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 57aaebe..9daee03 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -25,7 +25,7 @@ if (flutterVersionName == null) { android { namespace = "com.nankai.openpeerchat_flutter" - compileSdk = flutter.compileSdkVersion + compileSdk 34 ndkVersion = flutter.ndkVersion compileOptions { diff --git a/lib/main.dart b/lib/main.dart index 67ce4fc..41de4be 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,6 +8,41 @@ import 'classes/global.dart'; import 'encyption/key_storage.dart'; import 'encyption/rsa.dart'; +final ThemeData lightTheme = ThemeData( + brightness: Brightness.light, + primaryColor: Colors.blue, + scaffoldBackgroundColor: Colors.white, + textTheme: TextTheme( + displayLarge: TextStyle( + fontSize: 24.0, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + bodyLarge: TextStyle( + fontSize: 16.0, + color: Colors.black87, + ), + ), +); + +final ThemeData darkTheme = ThemeData( + brightness: Brightness.dark, + primaryColor: Colors.grey[900], + hintColor: Colors.blueAccent, + scaffoldBackgroundColor: Colors.grey[850], + textTheme: TextTheme( + displayLarge: TextStyle( + fontSize: 24.0, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + bodyLarge: TextStyle( + fontSize: 16.0, + color: Colors.white70, + ), + ), +); + void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -48,10 +83,13 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return const MaterialApp( + return MaterialApp( debugShowCheckedModeBanner: false, onGenerateRoute: generateRoute, initialRoute: '/', + theme: lightTheme, + darkTheme: darkTheme, + themeMode: ThemeMode.system, ); } } diff --git a/lib/pages/home_screen.dart b/lib/pages/home_screen.dart index ea87b16..1bfe894 100644 --- a/lib/pages/home_screen.dart +++ b/lib/pages/home_screen.dart @@ -4,6 +4,7 @@ import 'chat_list_screen.dart'; import '../classes/global.dart'; import '../p2p/adhoc_housekeeping.dart'; import 'device_list_screen.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import '../database/database_helper.dart'; @@ -12,6 +13,43 @@ import '../database/database_helper.dart'; /// As the app launches and navigates to the HomeScreen from the Profile screen, /// all the processes of message hopping are being initiated from this page. +const String themePreferenceKey = 'themePreference'; + +final ThemeData lightTheme = ThemeData( + brightness: Brightness.light, + primaryColor: Colors.blue, + scaffoldBackgroundColor: Colors.white, + textTheme: TextTheme( + displayLarge: TextStyle( + fontSize: 24.0, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + bodyLarge: TextStyle( + fontSize: 16.0, + color: Colors.black87, + ), + ), +); + +final ThemeData darkTheme = ThemeData( + brightness: Brightness.dark, + primaryColor: Colors.grey[900], + hintColor: Colors.blueAccent, + scaffoldBackgroundColor: Colors.grey[850], + textTheme: TextTheme( + displayLarge: TextStyle( + fontSize: 24.0, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + bodyLarge: TextStyle( + fontSize: 16.0, + color: Colors.white70, + ), + ), +); + class HomeScreen extends StatefulWidget { const HomeScreen({Key? key}) : super(key: key); @@ -22,11 +60,15 @@ class HomeScreen extends StatefulWidget { class _HomeScreenState extends State { bool isLoading = false; + //initial theme of the system + ThemeMode _themeMode = ThemeMode.system; + @override void initState() { super.initState(); // init(context); refreshMessages(); + _loadTheme(); } /// After reading all the cache, the home screen becomes visible. @@ -53,49 +95,86 @@ class _HomeScreenState extends State { super.dispose(); } + Future _loadTheme() async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + int? themeIndex = prefs.getInt(themePreferenceKey); + if (themeIndex != null) { + setState(() { + _themeMode = ThemeMode.values[themeIndex]; + }); + } + } + + Future _saveTheme(ThemeMode mode) async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.setInt(themePreferenceKey, mode.index); + } + + void _toggleTheme() { + setState(() { + _themeMode = _themeMode == ThemeMode.light + ? ThemeMode.dark + : ThemeMode.light; + }); + _saveTheme(_themeMode); + } + @override Widget build(BuildContext context) { return DefaultTabController( length: 2, - child: Scaffold( - key: Global.scaffoldKey, - appBar: AppBar( - title: const Text("AOSSIE"), - actions: [ - IconButton( - icon: const Icon(Icons.person), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const Profile( - onLogin: false, + child: Theme( + data: ThemeData( + brightness: _themeMode == ThemeMode.dark + ? Brightness.dark + : Brightness.light, + // You can further customize the theme here if needed + ), + child: Scaffold( + key: Global.scaffoldKey, + appBar: AppBar( + title: const Text("AOSSIE"), + actions: [ + //toggle button fro light and dark themes + IconButton( + onPressed: _toggleTheme, + icon: Icon(Icons.toggle_off_outlined), + ), + IconButton( + icon: const Icon(Icons.person), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const Profile( + onLogin: false, + ), ), - ), - ); - }, - ), - ], - bottom: const TabBar( - tabs: [ - Tab( - text: "Devices", + ); + }, ), - Tab( - text: "All Chats", + ], + bottom: const TabBar( + tabs: [ + Tab( + text: "Devices", + ), + Tab( + text: "All Chats", + ), + ], + ), + ), + body: const TabBarView( + children: [ + DevicesListScreen( + deviceType: DeviceType.browser, ), + ChatListScreen(), ], ), ), - body: const TabBarView( - children: [ - DevicesListScreen( - deviceType: DeviceType.browser, - ), - ChatListScreen(), - ], - ), ), ); } -} +} \ No newline at end of file diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 14cd431..9d4b458 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,7 +9,7 @@ import flutter_secure_storage_macos import local_auth_darwin import path_provider_foundation import shared_preferences_foundation -import sqflite +import sqflite_darwin func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) From 89de4713f800ee3a9c393400446fa5150b86f06f Mon Sep 17 00:00:00 2001 From: khushi-hura Date: Fri, 27 Dec 2024 23:07:54 +0530 Subject: [PATCH 2/7] Fixes issue #26 --- android/app/src/main/AndroidManifest.xml | 2 + lib/classes/audio_playback.dart | 27 ++++++ lib/classes/audio_recording.dart | 34 +++++++ lib/components/message_panel.dart | 110 +++++++++++++++++++++++ pubspec.yaml | 1 + 5 files changed, 174 insertions(+) create mode 100644 lib/classes/audio_playback.dart create mode 100644 lib/classes/audio_recording.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index eb48cb0..a72da72 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -11,6 +11,8 @@ + + initPlayer() async { + if(!_isInitialized){ + await _player.openPlayer(); + _isInitialized=true; + } + } + + Future playAudio(String filePath) async { + await _player.startPlayer(fromURI: filePath); + } + + Future stopAudio() async { + await _player.stopPlayer(); + } + + Future dispose() async { + if(_isInitialized){ + await _player.closePlayer(); + _isInitialized=false; + } + } +} diff --git a/lib/classes/audio_recording.dart b/lib/classes/audio_recording.dart new file mode 100644 index 0000000..99d7ade --- /dev/null +++ b/lib/classes/audio_recording.dart @@ -0,0 +1,34 @@ +import 'dart:io'; +import 'package:flutter_sound/flutter_sound.dart'; +import 'package:path_provider/path_provider.dart'; + +class AudioRecorder { + final FlutterSoundRecorder _recorder = FlutterSoundRecorder(); + bool _isRecording = false; + + Future initRecorder() async { + await _recorder.openRecorder(); + } + + Future startRecording() async { + if (!_isRecording) { + Directory tempDir = await getTemporaryDirectory(); + String filePath = '${tempDir.path}/audio_${DateTime.now().millisecondsSinceEpoch}.aac'; + await _recorder.startRecorder(toFile: filePath); + _isRecording = true; + return filePath; + } + return null; + } + + Future stopRecording() async { + if (_isRecording) { + await _recorder.stopRecorder(); + _isRecording = false; + } + } + + Future dispose() async { + await _recorder.closeRecorder(); + } +} diff --git a/lib/components/message_panel.dart b/lib/components/message_panel.dart index c5db219..eab3bd3 100644 --- a/lib/components/message_panel.dart +++ b/lib/components/message_panel.dart @@ -12,11 +12,20 @@ import '../classes/payload.dart'; import '../database/database_helper.dart'; import '../encyption/rsa.dart'; import 'view_file.dart'; +import '../classes/audio_playback.dart'; +import '../classes/audio_recording.dart'; +import 'package:permission_handler/permission_handler.dart'; /// This component is used in the ChatPage. /// It is the message bar where the message is typed on and sent to /// connected devices. +Future requestPermissions() async { + var micStatus = await Permission.microphone.request(); + var storageStatus = await Permission.storage.request(); + return micStatus.isGranted && storageStatus.isGranted; +} + class MessagePanel extends StatefulWidget { const MessagePanel({Key? key, required this.converser}) : super(key: key); final String converser; @@ -27,8 +36,17 @@ class MessagePanel extends StatefulWidget { class _MessagePanelState extends State { TextEditingController myController = TextEditingController(); + final AudioRecorder _audioRecorder = AudioRecorder(); + final AudioPlayer _audioPlayer = AudioPlayer(); + String? _recordingFilePath; + File _selectedFile = File(''); + void initState() { + super.initState(); + _audioRecorder.initRecorder(); + } + @override Widget build(BuildContext context) { return Padding( @@ -55,6 +73,18 @@ class _MessagePanelState extends State { Icons.send, ), ), + IconButton( + icon: Icon(Icons.mic), + onPressed: _startRecording, + ), + IconButton( + icon: Icon(Icons.stop), + onPressed: _stopRecording, + ), + IconButton( + icon: Icon(Icons.send), + onPressed: () => _sendAudioMessage(context), + ), ], ), ), @@ -62,6 +92,86 @@ class _MessagePanelState extends State { ); } + void _startRecording() async { + if (await requestPermissions()) { + print("***********************************"); + String? filePath = await _audioRecorder.startRecording(); + setState(() { + _recordingFilePath = filePath; + }); + } + } + + void _stopRecording() async { + await _audioRecorder.stopRecording(); + } + + void _playAudio() { + if (_recordingFilePath != null) { + _audioPlayer.playAudio(_recordingFilePath!); + } + } + + void _sendAudioMessage(BuildContext context) async { + // Ensure a recording exists + if (_recordingFilePath == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('No audio recorded to send.')), + ); + return; + } + + var msgId = nanoid(21); + + String fileName = _recordingFilePath!.split('/').last; + + // Encode audio metadata for the message + String data = jsonEncode({ + "sender": Global.myName, + "type": "audio", + "fileName": fileName, + "filePath": _recordingFilePath, + }); + + String date = DateTime.now().toUtc().toString(); + + // Save the message in cache + Global.cache[msgId] = Payload( + msgId, + Global.myName, + widget.converser, + data, + date, + ); + + // Insert the message into the database + insertIntoMessageTable( + Payload( + msgId, + Global.myName, + widget.converser, + data, + date, + ), + ); + + // Send the message to the conversation + Provider.of(context, listen: false).sentToConversations( + Msg(data, "sent", date, msgId), + widget.converser, + ); + + // Notify user and reset the recording state + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Audio message sent!')), + ); + + setState(() { + _recordingFilePath = null; // Clear the recording path after sending + }); +} + + void _sendMessage(BuildContext context) { var msgId = nanoid(21); if (myController.text.isEmpty) { diff --git a/pubspec.yaml b/pubspec.yaml index b3040f7..3d11fef 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -32,6 +32,7 @@ dependencies: open_filex: ^4.5.0 permission_handler: ^11.3.1 path_provider: ^2.1.4 + flutter_sound: dev_dependencies: flutter_lints: From 6b3bf0f9bd7b2a6bf299ae2058e2207502b8c932 Mon Sep 17 00:00:00 2001 From: khushi-hura Date: Sat, 28 Dec 2024 00:08:51 +0530 Subject: [PATCH 3/7] Fixes issue #26 --- macos/Flutter/GeneratedPluginRegistrant.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 14cd431..9d4b458 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,7 +9,7 @@ import flutter_secure_storage_macos import local_auth_darwin import path_provider_foundation import shared_preferences_foundation -import sqflite +import sqflite_darwin func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) From 1954e7abf27476c75ea8094f0c49169041c82872 Mon Sep 17 00:00:00 2001 From: khushi-hura Date: Sat, 28 Dec 2024 00:34:09 +0530 Subject: [PATCH 4/7] fixes issue #28 --- android/app/build.gradle | 2 +- lib/main.dart | 40 +++++++++- lib/pages/home_screen.dart | 145 ++++++++++++++++++++++++++++--------- 3 files changed, 152 insertions(+), 35 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 57aaebe..9daee03 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -25,7 +25,7 @@ if (flutterVersionName == null) { android { namespace = "com.nankai.openpeerchat_flutter" - compileSdk = flutter.compileSdkVersion + compileSdk 34 ndkVersion = flutter.ndkVersion compileOptions { diff --git a/lib/main.dart b/lib/main.dart index 67ce4fc..41de4be 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,6 +8,41 @@ import 'classes/global.dart'; import 'encyption/key_storage.dart'; import 'encyption/rsa.dart'; +final ThemeData lightTheme = ThemeData( + brightness: Brightness.light, + primaryColor: Colors.blue, + scaffoldBackgroundColor: Colors.white, + textTheme: TextTheme( + displayLarge: TextStyle( + fontSize: 24.0, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + bodyLarge: TextStyle( + fontSize: 16.0, + color: Colors.black87, + ), + ), +); + +final ThemeData darkTheme = ThemeData( + brightness: Brightness.dark, + primaryColor: Colors.grey[900], + hintColor: Colors.blueAccent, + scaffoldBackgroundColor: Colors.grey[850], + textTheme: TextTheme( + displayLarge: TextStyle( + fontSize: 24.0, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + bodyLarge: TextStyle( + fontSize: 16.0, + color: Colors.white70, + ), + ), +); + void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -48,10 +83,13 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return const MaterialApp( + return MaterialApp( debugShowCheckedModeBanner: false, onGenerateRoute: generateRoute, initialRoute: '/', + theme: lightTheme, + darkTheme: darkTheme, + themeMode: ThemeMode.system, ); } } diff --git a/lib/pages/home_screen.dart b/lib/pages/home_screen.dart index ea87b16..a8aad45 100644 --- a/lib/pages/home_screen.dart +++ b/lib/pages/home_screen.dart @@ -4,6 +4,7 @@ import 'chat_list_screen.dart'; import '../classes/global.dart'; import '../p2p/adhoc_housekeeping.dart'; import 'device_list_screen.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import '../database/database_helper.dart'; @@ -12,6 +13,43 @@ import '../database/database_helper.dart'; /// As the app launches and navigates to the HomeScreen from the Profile screen, /// all the processes of message hopping are being initiated from this page. +const String themePreferenceKey = 'themePreference'; + +final ThemeData lightTheme = ThemeData( + brightness:Brightness.light, + primaryColor: Colors.blue, + scaffoldBackgroundColor: Colors.white, + textTheme: TextTheme( + displayLarge: TextStyle( + fontSize: 24.0, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + bodyLarge: TextStyle( + fontSize: 16.0, + color: Colors.black87, + ), + ), +); + +final ThemeData darkTheme = ThemeData( + brightness:Brightness.dark, + primaryColor: Colors.grey[900], + hintColor: Colors.blueAccent, + scaffoldBackgroundColor: Colors.grey[850], + textTheme: TextTheme( + displayLarge: TextStyle( + fontSize: 24.0, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + bodyLarge: TextStyle( + fontSize: 16.0, + color: Colors.white70, + ), + ), +); + class HomeScreen extends StatefulWidget { const HomeScreen({Key? key}) : super(key: key); @@ -22,11 +60,15 @@ class HomeScreen extends StatefulWidget { class _HomeScreenState extends State { bool isLoading = false; + //initial theme of the system + ThemeMode _themeMode = ThemeMode.system; + @override void initState() { super.initState(); // init(context); refreshMessages(); + _loadTheme(); } /// After reading all the cache, the home screen becomes visible. @@ -53,49 +95,86 @@ class _HomeScreenState extends State { super.dispose(); } + Future _loadTheme() async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + int? themeIndex = prefs.getInt(themePreferenceKey); + if (themeIndex != null) { + setState(() { + _themeMode = ThemeMode.values[themeIndex]; + }); + } + } + + Future _saveTheme(ThemeMode mode) async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.setInt(themePreferenceKey, mode.index); + } + + void _toggleTheme() { + setState(() { + _themeMode = _themeMode == ThemeMode.light + ? ThemeMode.dark + : ThemeMode.light; + }); + _saveTheme(_themeMode); + } + @override Widget build(BuildContext context) { return DefaultTabController( length: 2, - child: Scaffold( - key: Global.scaffoldKey, - appBar: AppBar( - title: const Text("AOSSIE"), - actions: [ - IconButton( - icon: const Icon(Icons.person), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const Profile( - onLogin: false, + child: Theme( + data: ThemeData( + brightness: _themeMode == ThemeMode.dark + ? Brightness.dark + : Brightness.light, + // You can further customize the theme here if needed + ), + child: Scaffold( + key: Global.scaffoldKey, + appBar: AppBar( + title: const Text("AOSSIE"), + actions: [ + //toggle button fro light and dark themes + IconButton( + onPressed: _toggleTheme, + icon: Icon(Icons.toggle_off_outlined), + ), + IconButton( + icon: const Icon(Icons.person), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const Profile( + onLogin: false, + ), ), - ), - ); - }, - ), - ], - bottom: const TabBar( - tabs: [ - Tab( - text: "Devices", + ); + }, ), - Tab( - text: "All Chats", + ], + bottom: const TabBar( + tabs: [ + Tab( + text: "Devices", + ), + Tab( + text: "All Chats", + ), + ], + ), + ), + body: const TabBarView( + children: [ + DevicesListScreen( + deviceType: DeviceType.browser, ), + ChatListScreen(), ], ), ), - body: const TabBarView( - children: [ - DevicesListScreen( - deviceType: DeviceType.browser, - ), - ChatListScreen(), - ], - ), ), ); } -} +} \ No newline at end of file From ba28465bb22eaddb2b31d6185b31855859998954 Mon Sep 17 00:00:00 2001 From: khushi-hura Date: Sat, 28 Dec 2024 01:01:51 +0530 Subject: [PATCH 5/7] Fixes issue #28 --- lib/pages/home_screen.dart | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/lib/pages/home_screen.dart b/lib/pages/home_screen.dart index 1bfe894..756f205 100644 --- a/lib/pages/home_screen.dart +++ b/lib/pages/home_screen.dart @@ -9,7 +9,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import '../database/database_helper.dart'; /// This the home screen. This can also be considered as the -/// main screen of the application. +/// main screen of the application. /// As the app launches and navigates to the HomeScreen from the Profile screen, /// all the processes of message hopping are being initiated from this page. @@ -66,7 +66,6 @@ class _HomeScreenState extends State { @override void initState() { super.initState(); - // init(context); refreshMessages(); _loadTheme(); } @@ -110,11 +109,9 @@ class _HomeScreenState extends State { await prefs.setInt(themePreferenceKey, mode.index); } - void _toggleTheme() { + void _toggleTheme(bool value) { setState(() { - _themeMode = _themeMode == ThemeMode.light - ? ThemeMode.dark - : ThemeMode.light; + _themeMode = value ? ThemeMode.dark : ThemeMode.light; }); _saveTheme(_themeMode); } @@ -127,18 +124,19 @@ class _HomeScreenState extends State { data: ThemeData( brightness: _themeMode == ThemeMode.dark ? Brightness.dark - : Brightness.light, - // You can further customize the theme here if needed + : Brightness.light, ), child: Scaffold( key: Global.scaffoldKey, appBar: AppBar( title: const Text("AOSSIE"), actions: [ - //toggle button fro light and dark themes - IconButton( - onPressed: _toggleTheme, - icon: Icon(Icons.toggle_off_outlined), + // Slider toggle button for light and dark themes + Switch( + value: _themeMode == ThemeMode.dark, + onChanged: _toggleTheme, + activeColor: Colors.blueAccent, + inactiveThumbColor: Colors.grey, ), IconButton( icon: const Icon(Icons.person), @@ -177,4 +175,4 @@ class _HomeScreenState extends State { ), ); } -} \ No newline at end of file +} From 84171dc700efd94f2268e5751769cef585c01ee3 Mon Sep 17 00:00:00 2001 From: khushi-hura Date: Sat, 28 Dec 2024 01:15:15 +0530 Subject: [PATCH 6/7] Fixes issue #28 --- lib/pages/home_screen.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pages/home_screen.dart b/lib/pages/home_screen.dart index 756f205..73cd1f1 100644 --- a/lib/pages/home_screen.dart +++ b/lib/pages/home_screen.dart @@ -132,6 +132,7 @@ class _HomeScreenState extends State { title: const Text("AOSSIE"), actions: [ // Slider toggle button for light and dark themes + //added switch inplace of icon button Switch( value: _themeMode == ThemeMode.dark, onChanged: _toggleTheme, From b199c98d9868f5e071b82691b63d0c12e20cc445 Mon Sep 17 00:00:00 2001 From: khushi-hura Date: Sun, 12 Jan 2025 00:12:23 +0530 Subject: [PATCH 7/7] Fixes issue #28 --- android/app/src/main/AndroidManifest.xml | 1 + assets/audioAnimation.json | 1 + lib/components/message_panel.dart | 222 ++++++++++++++-------- lib/pages/chat_page.dart | 225 +++++++++++++++-------- pubspec.yaml | 3 + 5 files changed, 305 insertions(+), 147 deletions(-) create mode 100644 assets/audioAnimation.json diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index a72da72..f447eed 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -15,6 +15,7 @@ + requestPermissions() async { var micStatus = await Permission.microphone.request(); - var storageStatus = await Permission.storage.request(); - return micStatus.isGranted && storageStatus.isGranted; + return micStatus.isGranted; } class MessagePanel extends StatefulWidget { @@ -39,71 +35,107 @@ class _MessagePanelState extends State { final AudioRecorder _audioRecorder = AudioRecorder(); final AudioPlayer _audioPlayer = AudioPlayer(); String? _recordingFilePath; - + bool _isRecording = false; + Duration _recordingDuration = Duration.zero; File _selectedFile = File(''); void initState() { super.initState(); _audioRecorder.initRecorder(); - } - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: TextFormField( - //multiline text field - maxLines: null, - controller: myController, - decoration: InputDecoration( - icon: const Icon(Icons.person), - hintText: 'Send Message?', - labelText: 'Send Message ', - suffixIcon: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - onPressed: () => _navigateToFilePreviewPage(context), - icon: const Icon(Icons.attach_file), - ), - IconButton( - onPressed: () => _sendMessage(context), - icon: const Icon( - Icons.send, - ), - ), - IconButton( - icon: Icon(Icons.mic), - onPressed: _startRecording, - ), - IconButton( - icon: Icon(Icons.stop), - onPressed: _stopRecording, - ), - IconButton( - icon: Icon(Icons.send), - onPressed: () => _sendAudioMessage(context), - ), - ], - ), - ), - ), - ); + _audioPlayer.initPlayer(); } void _startRecording() async { if (await requestPermissions()) { - print("***********************************"); String? filePath = await _audioRecorder.startRecording(); setState(() { _recordingFilePath = filePath; + _isRecording = true; + _recordingDuration = Duration.zero; }); + _startRecordingTimer(); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Microphone permission denied.')), + ); } } - void _stopRecording() async { + void _stopRecording({bool cancel = false}) async { await _audioRecorder.stopRecording(); + setState(() { + _isRecording = false; + if (cancel) { + _recordingFilePath = null; + } + }); + + if (!cancel && _recordingFilePath != null) { + _confirmAudioMessage(); + } + } + + void _startRecordingTimer() { + Future.doWhile(() async { + await Future.delayed(const Duration(seconds: 1)); + if (_isRecording) { + setState(() { + _recordingDuration += const Duration(seconds: 1); + }); + return true; + } + return false; + }); + } + + void _confirmAudioMessage() { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text('Send Audio Message'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Duration: ${_recordingDuration.inSeconds} seconds'), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + IconButton( + icon: const Icon(Icons.play_arrow), + onPressed: () => _playAudio(), + ), + Lottie.asset('assets/audioAnimation.json', + height: 50, width: 50), + IconButton( + icon: const Icon(Icons.delete), + onPressed: () { + setState(() { + _recordingFilePath = null; + }); + Navigator.pop(context); + }, + ), + ], + ) + ], + ), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context); + _sendAudioMessage(); + }, + child: const Text('Send'), + ), + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancel'), + ), + ], + ); + }, + ); } void _playAudio() { @@ -112,66 +144,108 @@ class _MessagePanelState extends State { } } - void _sendAudioMessage(BuildContext context) async { - // Ensure a recording exists - if (_recordingFilePath == null) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('No audio recorded to send.')), - ); - return; - } + void _sendAudioMessage() async { + if (_recordingFilePath == null) return; - var msgId = nanoid(21); + // Read the audio file content + Uint8List audioBytes = await File(_recordingFilePath!).readAsBytes(); + // Encrypt the audio file content + RSAPublicKey publicKey = Global.myPublicKey!; + Uint8List encryptedAudio = rsaEncrypt(publicKey, audioBytes); + + // Generate a unique ID for the message + var msgId = nanoid(21); String fileName = _recordingFilePath!.split('/').last; - // Encode audio metadata for the message - String data = jsonEncode({ + // Create the encrypted message payload + String myData = jsonEncode({ "sender": Global.myName, "type": "audio", "fileName": fileName, - "filePath": _recordingFilePath, + "data": base64Encode(encryptedAudio), }); String date = DateTime.now().toUtc().toString(); - // Save the message in cache + // Add the message to the local cache Global.cache[msgId] = Payload( msgId, Global.myName, widget.converser, - data, + myData, date, ); - // Insert the message into the database + // Save the message to the database insertIntoMessageTable( Payload( msgId, Global.myName, widget.converser, - data, + myData, date, ), ); - // Send the message to the conversation + // Update the conversations in the UI Provider.of(context, listen: false).sentToConversations( - Msg(data, "sent", date, msgId), + Msg(myData, "sent", date, msgId), widget.converser, ); - // Notify user and reset the recording state + // Provide feedback to the user ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Audio message sent!')), ); + // Reset the recording state setState(() { - _recordingFilePath = null; // Clear the recording path after sending + _recordingFilePath = null; + _recordingDuration = Duration.zero; }); } + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: TextFormField( + maxLines: null, + controller: myController, + decoration: InputDecoration( + icon: const Icon(Icons.person), + hintText: 'Send Message?', + labelText: 'Send Message', + suffixIcon: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: () => _navigateToFilePreviewPage(context), + icon: const Icon(Icons.attach_file), + ), + GestureDetector( + onLongPress: _startRecording, + onLongPressUp: () => _stopRecording(cancel: false), + onTapCancel: () => _stopRecording(cancel: true), + child: Icon( + _isRecording ? Icons.mic : Icons.mic_none, + color: _isRecording ? Colors.red : Colors.black, + ), + ), + IconButton( + onPressed: () => _sendMessage(context), + icon: const Icon(Icons.send), + ), + ], + ), + ), + ), + ); + } + void _sendMessage(BuildContext context) { var msgId = nanoid(21); if (myController.text.isEmpty) { diff --git a/lib/pages/chat_page.dart b/lib/pages/chat_page.dart index 33a2518..d7c2bdc 100644 --- a/lib/pages/chat_page.dart +++ b/lib/pages/chat_page.dart @@ -11,6 +11,44 @@ import 'dart:convert'; import 'package:pointycastle/asymmetric/api.dart'; import '../components/view_file.dart'; import '../encyption/rsa.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +const String themePreferenceKey = 'themePreference'; + +final ThemeData lightTheme = ThemeData( + brightness: Brightness.light, + primaryColor: Colors.blue, + scaffoldBackgroundColor: Colors.white, + textTheme: TextTheme( + displayLarge: TextStyle( + fontSize: 24.0, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + bodyLarge: TextStyle( + fontSize: 16.0, + color: Colors.black87, + ), + ), +); + +final ThemeData darkTheme = ThemeData( + brightness: Brightness.dark, + primaryColor: Colors.grey[900], + hintColor: Colors.blueAccent, + scaffoldBackgroundColor: Colors.grey[850], + textTheme: TextTheme( + displayLarge: TextStyle( + fontSize: 24.0, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + bodyLarge: TextStyle( + fontSize: 16.0, + color: Colors.white70, + ), + ), +); class ChatPage extends StatefulWidget { const ChatPage({Key? key, required this.converser}) : super(key: key); @@ -24,10 +62,14 @@ class ChatPage extends StatefulWidget { class ChatPageState extends State { List messageList = []; TextEditingController myController = TextEditingController(); + + //initial theme of the system + ThemeMode _themeMode = ThemeMode.system; @override void initState() { super.initState(); + _loadTheme(); } @override @@ -35,6 +77,28 @@ class ChatPageState extends State { super.didChangeDependencies(); } + Future _loadTheme() async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + int? themeIndex = prefs.getInt(themePreferenceKey); + if (themeIndex != null) { + setState(() { + _themeMode = ThemeMode.values[themeIndex]; + }); + } + } + + Future _saveTheme(ThemeMode mode) async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.setInt(themePreferenceKey, mode.index); + } + + void _toggleTheme(bool value) { + setState(() { + _themeMode = value ? ThemeMode.dark : ThemeMode.light; + }); + _saveTheme(_themeMode); + } + final ScrollController _scrollController = ScrollController(); @override @@ -64,86 +128,101 @@ class ChatPageState extends State { } groupedMessages[date]!.add(msg); } - - return Scaffold( - appBar: AppBar( - title: Text(widget.converser), - ), - body: Column( - children: [ - Expanded( - child: messageList.isEmpty - ? 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), + return Theme( + data: ThemeData( + brightness: _themeMode == ThemeMode.dark + ? Brightness.dark + : Brightness.light, + ), + child: + Scaffold( + appBar: AppBar( + title: Text(widget.converser), + actions: [ + Switch( + value: _themeMode == ThemeMode.dark, + onChanged: _toggleTheme, + activeColor: Colors.blueAccent, + inactiveThumbColor: Colors.grey, + ), + ], + ), + body: Column( + children: [ + Expanded( + child: messageList.isEmpty + ? 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), + ), ), ), - ), - ...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); + ...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), + 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), + 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), - ], - ), + MessagePanel(converser: widget.converser), + ], + ), + ) ); } diff --git a/pubspec.yaml b/pubspec.yaml index 3d11fef..f189bf4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -33,6 +33,7 @@ dependencies: permission_handler: ^11.3.1 path_provider: ^2.1.4 flutter_sound: + lottie: dev_dependencies: flutter_lints: @@ -40,4 +41,6 @@ dev_dependencies: sdk: flutter flutter: + assets: + - assets/audioAnimation.json uses-material-design: true \ No newline at end of file