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/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/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")) 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: