2021-03-12 19:52:26 +00:00
|
|
|
import 'dart:io';
|
|
|
|
import 'dart:typed_data';
|
|
|
|
|
|
|
|
import 'package:comunic/ui/dialogs/alert_dialog.dart';
|
|
|
|
import 'package:comunic/utils/intl_utils.dart';
|
|
|
|
import 'package:comunic/utils/log_utils.dart';
|
2021-03-12 20:19:40 +00:00
|
|
|
import 'package:comunic/utils/permission_utils.dart';
|
2021-03-12 19:52:26 +00:00
|
|
|
import 'package:comunic/utils/ui_utils.dart';
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:path/path.dart' as path;
|
|
|
|
import 'package:path_provider/path_provider.dart';
|
2021-03-12 20:19:40 +00:00
|
|
|
import 'package:permission_handler/permission_handler.dart';
|
|
|
|
import 'package:record_mp3/record_mp3.dart';
|
2021-03-12 19:52:26 +00:00
|
|
|
import 'package:video_player/video_player.dart';
|
|
|
|
|
|
|
|
/// Record audio dialog
|
|
|
|
///
|
|
|
|
/// @author Pierre Hubert
|
|
|
|
|
|
|
|
/// Record audio
|
2022-03-10 18:39:57 +00:00
|
|
|
Future<Uint8List?> showRecordAudioDialog(BuildContext context) async {
|
2021-03-12 20:19:40 +00:00
|
|
|
// Request record permission
|
|
|
|
if (!await requestPermission(context, Permission.microphone)) {
|
|
|
|
alert(context, tr("Did not get permission to access microphone!"));
|
2021-03-12 19:52:26 +00:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
final res = await showDialog(
|
|
|
|
context: context,
|
|
|
|
builder: (c) => Scaffold(
|
|
|
|
body: _RecordAudioDialog(),
|
|
|
|
backgroundColor: Colors.transparent,
|
|
|
|
),
|
|
|
|
barrierDismissible: false,
|
|
|
|
);
|
|
|
|
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
|
|
|
class _RecordAudioDialog extends StatefulWidget {
|
|
|
|
@override
|
|
|
|
__RecordAudioDialogState createState() => __RecordAudioDialogState();
|
|
|
|
}
|
|
|
|
|
|
|
|
class __RecordAudioDialogState extends State<_RecordAudioDialog> {
|
2022-03-10 18:39:57 +00:00
|
|
|
String? _recordPath;
|
2021-03-12 19:52:26 +00:00
|
|
|
|
2022-03-10 18:39:57 +00:00
|
|
|
File? get _recordFile => _recordPath == null ? null : File(_recordPath!);
|
2021-03-12 19:52:26 +00:00
|
|
|
|
|
|
|
bool _recording = false;
|
|
|
|
|
|
|
|
bool get _hasRecord => !_recording && _recordPath != null;
|
|
|
|
|
2022-03-10 18:39:57 +00:00
|
|
|
VideoPlayerController? _videoPlayerController;
|
2021-03-12 19:52:26 +00:00
|
|
|
|
|
|
|
bool _playing = false;
|
|
|
|
|
|
|
|
bool _paused = false;
|
|
|
|
|
|
|
|
/// Get record data. This getter can be accessed only once
|
|
|
|
Uint8List get _bytes {
|
2022-03-10 18:39:57 +00:00
|
|
|
final bytes = _recordFile!.readAsBytesSync();
|
|
|
|
File(_recordPath!).deleteSync();
|
2021-03-12 19:52:26 +00:00
|
|
|
return bytes;
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void dispose() {
|
|
|
|
_disposePlayer();
|
2021-03-12 20:19:40 +00:00
|
|
|
RecordMp3.instance.stop();
|
2021-03-12 19:52:26 +00:00
|
|
|
super.dispose();
|
|
|
|
}
|
|
|
|
|
|
|
|
void _disposePlayer() {
|
|
|
|
if (_videoPlayerController != null) {
|
2022-03-10 18:39:57 +00:00
|
|
|
_videoPlayerController!.dispose();
|
2021-03-12 19:52:26 +00:00
|
|
|
}
|
|
|
|
_videoPlayerController = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
return AlertDialog(
|
2022-03-10 18:39:57 +00:00
|
|
|
title: Text(tr("Audio record")!),
|
2021-03-12 19:52:26 +00:00
|
|
|
content: _buildContent(),
|
|
|
|
actions: <Widget>[
|
|
|
|
_ActionButton(
|
|
|
|
visible: !_recording,
|
2022-03-10 18:39:57 +00:00
|
|
|
text: tr("Cancel")!,
|
2021-03-12 19:52:26 +00:00
|
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
|
|
),
|
|
|
|
_ActionButton(
|
|
|
|
visible: _hasRecord,
|
2022-03-10 18:39:57 +00:00
|
|
|
text: tr("Send")!,
|
2021-03-12 19:52:26 +00:00
|
|
|
onPressed: () => Navigator.of(context).pop(_bytes),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-03-10 18:39:57 +00:00
|
|
|
String? get _status {
|
2021-03-12 19:52:26 +00:00
|
|
|
if (_recording)
|
|
|
|
return tr("Recording...");
|
|
|
|
else if (_paused)
|
|
|
|
return tr("Playback paused...");
|
|
|
|
else if (_playing)
|
|
|
|
return tr("Playing...");
|
|
|
|
else if (!_hasRecord)
|
|
|
|
return tr("Ready");
|
|
|
|
else
|
|
|
|
return tr("Done");
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget _buildContent() => Row(
|
|
|
|
children: <Widget>[
|
2022-03-10 18:39:57 +00:00
|
|
|
Text(_status!),
|
2021-03-12 19:52:26 +00:00
|
|
|
|
|
|
|
Spacer(),
|
|
|
|
|
|
|
|
// Start recording
|
|
|
|
_RecordAction(
|
|
|
|
visible: !_recording && !_playing,
|
|
|
|
icon: Icons.fiber_manual_record,
|
|
|
|
onTap: _startRecording,
|
|
|
|
color: Colors.red,
|
|
|
|
),
|
|
|
|
|
|
|
|
// Stop recording
|
|
|
|
_RecordAction(
|
|
|
|
visible: _recording,
|
|
|
|
icon: Icons.stop,
|
|
|
|
onTap: _stopRecording,
|
|
|
|
color: Colors.red,
|
|
|
|
),
|
|
|
|
|
|
|
|
// Play recording
|
|
|
|
_RecordAction(
|
|
|
|
visible: !_recording && _hasRecord && !_playing,
|
|
|
|
icon: Icons.play_arrow,
|
|
|
|
onTap: _playRecord,
|
|
|
|
),
|
|
|
|
|
|
|
|
// Pause playback
|
|
|
|
_RecordAction(
|
|
|
|
visible: _playing && !_paused,
|
|
|
|
icon: Icons.pause,
|
|
|
|
onTap: _pausePlayback,
|
|
|
|
),
|
|
|
|
|
|
|
|
// Resume recording
|
|
|
|
_RecordAction(
|
|
|
|
visible: _paused,
|
|
|
|
icon: Icons.play_arrow,
|
|
|
|
onTap: _resumePlayback,
|
|
|
|
),
|
|
|
|
|
|
|
|
// Stop recording
|
|
|
|
_RecordAction(
|
|
|
|
visible: _playing,
|
|
|
|
icon: Icons.stop,
|
|
|
|
onTap: _stopPlayback,
|
|
|
|
),
|
|
|
|
],
|
|
|
|
);
|
|
|
|
|
|
|
|
void _startRecording() async {
|
|
|
|
try {
|
2022-03-10 18:39:57 +00:00
|
|
|
if (_recordFile != null) _recordFile!.deleteSync();
|
2021-03-12 19:52:26 +00:00
|
|
|
|
|
|
|
final dir = await getTemporaryDirectory();
|
|
|
|
|
2021-03-12 20:19:40 +00:00
|
|
|
_recordPath = path.join(dir.absolute.path, "tmp-audio-record.mp3");
|
2021-03-12 19:52:26 +00:00
|
|
|
|
2022-03-10 18:39:57 +00:00
|
|
|
RecordMp3.instance.start(_recordPath!, (fail) {
|
2021-03-12 20:19:40 +00:00
|
|
|
print(fail);
|
2022-03-10 18:39:57 +00:00
|
|
|
snack(context, tr("Failed to start recording!")!);
|
2021-03-12 20:19:40 +00:00
|
|
|
});
|
2021-03-12 19:52:26 +00:00
|
|
|
|
|
|
|
setState(() => _recording = true);
|
|
|
|
} catch (e, s) {
|
|
|
|
logError(e, s);
|
2022-03-10 18:39:57 +00:00
|
|
|
snack(context, tr("Error while recording!")!);
|
2021-03-12 19:52:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void _stopRecording() async {
|
|
|
|
try {
|
2021-03-12 20:19:40 +00:00
|
|
|
RecordMp3.instance.stop();
|
2021-03-12 19:52:26 +00:00
|
|
|
|
|
|
|
setState(() => _recording = false);
|
|
|
|
} catch (e, s) {
|
|
|
|
logError(e, s);
|
2022-03-10 18:39:57 +00:00
|
|
|
snack(context, tr("Error while recording!")!);
|
2021-03-12 19:52:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void _playRecord() async {
|
|
|
|
try {
|
|
|
|
_disposePlayer();
|
|
|
|
|
2022-03-10 18:39:57 +00:00
|
|
|
_videoPlayerController = VideoPlayerController.file(File(_recordPath!));
|
|
|
|
await _videoPlayerController!.initialize();
|
2021-03-12 19:52:26 +00:00
|
|
|
|
2022-03-10 18:39:57 +00:00
|
|
|
_videoPlayerController!.addListener(() async {
|
2021-03-12 19:52:26 +00:00
|
|
|
if (_videoPlayerController == null) return;
|
|
|
|
|
2022-03-10 18:39:57 +00:00
|
|
|
if (_videoPlayerController!.value.position ==
|
|
|
|
_videoPlayerController!.value.duration) _stopPlayback();
|
2021-03-12 19:52:26 +00:00
|
|
|
});
|
|
|
|
|
2022-03-10 18:39:57 +00:00
|
|
|
await _videoPlayerController!.play();
|
2021-03-12 19:52:26 +00:00
|
|
|
|
|
|
|
setState(() {
|
|
|
|
_playing = true;
|
|
|
|
_paused = false;
|
|
|
|
});
|
|
|
|
} catch (e, s) {
|
|
|
|
logError(e, s);
|
2022-03-10 18:39:57 +00:00
|
|
|
snack(context, tr("Error while playing record!")!);
|
2021-03-12 19:52:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void _pausePlayback() async {
|
|
|
|
try {
|
2022-03-10 18:39:57 +00:00
|
|
|
await _videoPlayerController!.pause();
|
2021-03-12 19:52:26 +00:00
|
|
|
setState(() => _paused = true);
|
|
|
|
} catch (e, s) {
|
|
|
|
logError(e, s);
|
2022-03-10 18:39:57 +00:00
|
|
|
snack(context, tr("Error while pausing playback!")!);
|
2021-03-12 19:52:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void _resumePlayback() async {
|
|
|
|
try {
|
2022-03-10 18:39:57 +00:00
|
|
|
await _videoPlayerController!.play();
|
2021-03-12 19:52:26 +00:00
|
|
|
setState(() => _paused = false);
|
|
|
|
} catch (e, s) {
|
|
|
|
logError(e, s);
|
2022-03-10 18:39:57 +00:00
|
|
|
snack(context, tr("Error while resuming playback!")!);
|
2021-03-12 19:52:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void _stopPlayback() async {
|
|
|
|
try {
|
|
|
|
_disposePlayer();
|
|
|
|
setState(() {
|
|
|
|
_paused = false;
|
|
|
|
_playing = false;
|
|
|
|
});
|
|
|
|
} catch (e, s) {
|
|
|
|
logError(e, s);
|
2022-03-10 18:39:57 +00:00
|
|
|
snack(context, tr("Error while stopping playback!")!);
|
2021-03-12 19:52:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class _RecordAction extends StatelessWidget {
|
|
|
|
final bool visible;
|
|
|
|
final IconData icon;
|
|
|
|
final void Function() onTap;
|
2022-03-10 18:39:57 +00:00
|
|
|
final Color? color;
|
2021-03-12 19:52:26 +00:00
|
|
|
|
|
|
|
const _RecordAction({
|
2022-03-10 18:39:57 +00:00
|
|
|
Key? key,
|
|
|
|
required this.visible,
|
|
|
|
required this.icon,
|
|
|
|
required this.onTap,
|
2021-03-12 19:52:26 +00:00
|
|
|
this.color,
|
|
|
|
}) : assert(visible != null),
|
|
|
|
assert(icon != null),
|
|
|
|
super(key: key);
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
if (!visible) return Container(width: 0, height: 0);
|
|
|
|
return IconButton(icon: Icon(icon, color: color), onPressed: onTap);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class _ActionButton extends StatelessWidget {
|
|
|
|
final bool visible;
|
|
|
|
final String text;
|
2022-03-10 18:39:57 +00:00
|
|
|
final void Function()? onPressed;
|
2021-03-12 19:52:26 +00:00
|
|
|
|
|
|
|
const _ActionButton({
|
2022-03-10 18:39:57 +00:00
|
|
|
Key? key,
|
|
|
|
required this.visible,
|
|
|
|
required this.text,
|
2021-03-12 19:52:26 +00:00
|
|
|
this.onPressed,
|
|
|
|
}) : assert(visible != null),
|
|
|
|
assert(text != null),
|
|
|
|
super(key: key);
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
if (!visible) return Container();
|
|
|
|
return MaterialButton(
|
|
|
|
onPressed: onPressed,
|
|
|
|
child: Text(text.toUpperCase()),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|