Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ if (flutterVersionName == null) {

android {
namespace = "com.nankai.openpeerchat_flutter"
compileSdk = flutter.compileSdkVersion
compileSdk 34
ndkVersion = flutter.ndkVersion

compileOptions {
Expand Down
3 changes: 3 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@
<uses-permission android:minSdkVersion="31" android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:minSdkVersion="31" android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:minSdkVersion="32" android:name="android.permission.NEARBY_WIFI_DEVICES" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- Optional: only required for FILE payloads -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
<application
android:label="openpeerchat_flutter"
android:name="${applicationName}"
Expand Down
1 change: 1 addition & 0 deletions assets/audioAnimation.json

Large diffs are not rendered by default.

27 changes: 27 additions & 0 deletions lib/classes/audio_playback.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import 'package:flutter_sound/flutter_sound.dart';
class AudioPlayer {
final FlutterSoundPlayer _player = FlutterSoundPlayer();
bool _isInitialized=false;

Future<void> initPlayer() async {
if(!_isInitialized){
await _player.openPlayer();
_isInitialized=true;
}
}

Future<void> playAudio(String filePath) async {
await _player.startPlayer(fromURI: filePath);
}

Future<void> stopAudio() async {
await _player.stopPlayer();
}

Future<void> dispose() async {
if(_isInitialized){
await _player.closePlayer();
_isInitialized=false;
}
}
}
34 changes: 34 additions & 0 deletions lib/classes/audio_recording.dart
Original file line number Diff line number Diff line change
@@ -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<void> initRecorder() async {
await _recorder.openRecorder();
}

Future<String?> 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<void> stopRecording() async {
if (_isRecording) {
await _recorder.stopRecorder();
_isRecording = false;
}
}

Future<void> dispose() async {
await _recorder.closeRecorder();
}
}
200 changes: 192 additions & 8 deletions lib/components/message_panel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:lottie/lottie.dart';
import 'package:nanoid/nanoid.dart';
import 'package:pointycastle/asymmetric/api.dart';
import 'package:provider/provider.dart';
Expand All @@ -12,10 +13,14 @@ 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<bool> requestPermissions() async {
var micStatus = await Permission.microphone.request();
return micStatus.isGranted;
}

class MessagePanel extends StatefulWidget {
const MessagePanel({Key? key, required this.converser}) : super(key: key);
Expand All @@ -27,20 +32,192 @@ class MessagePanel extends StatefulWidget {

class _MessagePanelState extends State<MessagePanel> {
TextEditingController myController = TextEditingController();
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();
_audioPlayer.initPlayer();
}

void _startRecording() async {
if (await requestPermissions()) {
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({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() {
if (_recordingFilePath != null) {
_audioPlayer.playAudio(_recordingFilePath!);
}
}

void _sendAudioMessage() async {
if (_recordingFilePath == null) return;

// 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;

// Create the encrypted message payload
String myData = jsonEncode({
"sender": Global.myName,
"type": "audio",
"fileName": fileName,
"data": base64Encode(encryptedAudio),
});

String date = DateTime.now().toUtc().toString();

// Add the message to the local cache
Global.cache[msgId] = Payload(
msgId,
Global.myName,
widget.converser,
myData,
date,
);

// Save the message to the database
insertIntoMessageTable(
Payload(
msgId,
Global.myName,
widget.converser,
myData,
date,
),
);

// Update the conversations in the UI
Provider.of<Global>(context, listen: false).sentToConversations(
Msg(myData, "sent", date, msgId),
widget.converser,
);

// Provide feedback to the user
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Audio message sent!')),
);

// Reset the recording state
setState(() {
_recordingFilePath = null;
_recordingDuration = Duration.zero;
});
}


@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: TextFormField(
//multiline text field
maxLines: null,
controller: myController,
decoration: InputDecoration(
icon: const Icon(Icons.person),
hintText: 'Send Message?',
labelText: 'Send Message ',
labelText: 'Send Message',
suffixIcon: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
Expand All @@ -49,11 +226,18 @@ class _MessagePanelState extends State<MessagePanel> {
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,
),
icon: const Icon(Icons.send),
),
],
),
Expand Down
40 changes: 39 additions & 1 deletion lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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,
);
}
}
Expand Down
Loading