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'; import 'package:comunic/utils/permission_utils.dart'; 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'; import 'package:permission_handler/permission_handler.dart'; import 'package:record_mp3/record_mp3.dart'; import 'package:video_player/video_player.dart'; /// Record audio dialog /// /// @author Pierre Hubert /// Record audio Future showRecordAudioDialog(BuildContext context) async { // Request record permission if (!await requestPermission(context, Permission.microphone)) { alert(context, tr("Did not get permission to access microphone!")); 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> { String? _recordPath; File? get _recordFile => _recordPath == null ? null : File(_recordPath!); bool _recording = false; bool get _hasRecord => !_recording && _recordPath != null; VideoPlayerController? _videoPlayerController; bool _playing = false; bool _paused = false; /// Get record data. This getter can be accessed only once Uint8List get _bytes { final bytes = _recordFile!.readAsBytesSync(); File(_recordPath!).deleteSync(); return bytes; } @override void dispose() { _disposePlayer(); RecordMp3.instance.stop(); super.dispose(); } void _disposePlayer() { if (_videoPlayerController != null) { _videoPlayerController!.dispose(); } _videoPlayerController = null; } @override Widget build(BuildContext context) { return AlertDialog( title: Text(tr("Audio record")!), content: _buildContent(), actions: [ _ActionButton( visible: !_recording, text: tr("Cancel")!, onPressed: () => Navigator.of(context).pop(), ), _ActionButton( visible: _hasRecord, text: tr("Send")!, onPressed: () => Navigator.of(context).pop(_bytes), ), ], ); } String? get _status { 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: [ Text(_status!), 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 { if (_recordFile != null) _recordFile!.deleteSync(); final dir = await getTemporaryDirectory(); _recordPath = path.join(dir.absolute.path, "tmp-audio-record.mp3"); RecordMp3.instance.start(_recordPath!, (fail) { print(fail); snack(context, tr("Failed to start recording!")!); }); setState(() => _recording = true); } catch (e, s) { logError(e, s); snack(context, tr("Error while recording!")!); } } void _stopRecording() async { try { RecordMp3.instance.stop(); setState(() => _recording = false); } catch (e, s) { logError(e, s); snack(context, tr("Error while recording!")!); } } void _playRecord() async { try { _disposePlayer(); _videoPlayerController = VideoPlayerController.file(File(_recordPath!)); await _videoPlayerController!.initialize(); _videoPlayerController!.addListener(() async { if (_videoPlayerController == null) return; if (_videoPlayerController!.value.position == _videoPlayerController!.value.duration) _stopPlayback(); }); await _videoPlayerController!.play(); setState(() { _playing = true; _paused = false; }); } catch (e, s) { logError(e, s); snack(context, tr("Error while playing record!")!); } } void _pausePlayback() async { try { await _videoPlayerController!.pause(); setState(() => _paused = true); } catch (e, s) { logError(e, s); snack(context, tr("Error while pausing playback!")!); } } void _resumePlayback() async { try { await _videoPlayerController!.play(); setState(() => _paused = false); } catch (e, s) { logError(e, s); snack(context, tr("Error while resuming playback!")!); } } void _stopPlayback() async { try { _disposePlayer(); setState(() { _paused = false; _playing = false; }); } catch (e, s) { logError(e, s); snack(context, tr("Error while stopping playback!")!); } } } class _RecordAction extends StatelessWidget { final bool visible; final IconData icon; final void Function() onTap; final Color? color; const _RecordAction({ Key? key, required this.visible, required this.icon, required this.onTap, 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; final void Function()? onPressed; const _ActionButton({ Key? key, required this.visible, required this.text, 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()), ); } }