mirror of
https://gitlab.com/comunic/comunicmobile
synced 2024-11-22 12:59:21 +00:00
Start to record MP3 files
This commit is contained in:
parent
6fc1a263d2
commit
e7b1beca50
30
lib/ui/dialogs/alert_dialog.dart
Normal file
30
lib/ui/dialogs/alert_dialog.dart
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Simple alert dialog
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
Future<void> alert(BuildContext context, String msg) async {
|
||||||
|
await showDialog(context: context, builder: (c) => _AlertDialog(msg: msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AlertDialog extends StatelessWidget {
|
||||||
|
final String msg;
|
||||||
|
|
||||||
|
const _AlertDialog({Key key, @required this.msg})
|
||||||
|
: assert(msg != null),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
content: Text(msg),
|
||||||
|
actions: <Widget>[
|
||||||
|
MaterialButton(
|
||||||
|
child: Text("OK"),
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:comunic/models/api_request.dart';
|
import 'package:comunic/models/api_request.dart';
|
||||||
|
import 'package:comunic/ui/dialogs/record_audio_dialog.dart';
|
||||||
import 'package:comunic/utils/files_utils.dart';
|
import 'package:comunic/utils/files_utils.dart';
|
||||||
import 'package:comunic/utils/intl_utils.dart';
|
import 'package:comunic/utils/intl_utils.dart';
|
||||||
import 'package:comunic/utils/ui_utils.dart';
|
import 'package:comunic/utils/ui_utils.dart';
|
||||||
@ -17,6 +18,7 @@ enum _FileChoices {
|
|||||||
TAKE_PICTURE,
|
TAKE_PICTURE,
|
||||||
PICK_VIDEO,
|
PICK_VIDEO,
|
||||||
TAKE_VIDEO,
|
TAKE_VIDEO,
|
||||||
|
RECORD_AUDIO,
|
||||||
PICK_OTHER_FILE,
|
PICK_OTHER_FILE,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,12 +67,20 @@ List<_PickFileOption> get _optionsList => [
|
|||||||
icon: Icons.videocam,
|
icon: Icons.videocam,
|
||||||
canEnable: (l) => l.any(isVideo)),
|
canEnable: (l) => l.any(isVideo)),
|
||||||
|
|
||||||
|
// Audio
|
||||||
|
_PickFileOption(
|
||||||
|
value: _FileChoices.RECORD_AUDIO,
|
||||||
|
label: tr("Record audio"),
|
||||||
|
icon: Icons.mic,
|
||||||
|
canEnable: (l) => l.any(isAudio)),
|
||||||
|
|
||||||
// Other
|
// Other
|
||||||
_PickFileOption(
|
_PickFileOption(
|
||||||
value: _FileChoices.PICK_OTHER_FILE,
|
value: _FileChoices.PICK_OTHER_FILE,
|
||||||
label: tr("Browse files"),
|
label: tr("Browse files"),
|
||||||
icon: Icons.folder_open,
|
icon: Icons.folder_open,
|
||||||
canEnable: (l) => l.any((el) => !isImage(el) && !isVideo(el))),
|
canEnable: (l) =>
|
||||||
|
l.any((el) => !isImage(el) && !isVideo(el) && !isAudio(el))),
|
||||||
];
|
];
|
||||||
|
|
||||||
Future<BytesFile> showPickFileDialog({
|
Future<BytesFile> showPickFileDialog({
|
||||||
@ -138,6 +148,13 @@ Future<BytesFile> showPickFileDialog({
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
// Record audio file
|
||||||
|
case _FileChoices.RECORD_AUDIO:
|
||||||
|
final bytes = await showRecordAudioDialog(context);
|
||||||
|
if (bytes == null) return null;
|
||||||
|
file = BytesFile("record.mp3", bytes);
|
||||||
|
break;
|
||||||
|
|
||||||
// Pick other files
|
// Pick other files
|
||||||
case _FileChoices.PICK_OTHER_FILE:
|
case _FileChoices.PICK_OTHER_FILE:
|
||||||
final pickedFile = await FilePicker.platform.pickFiles(
|
final pickedFile = await FilePicker.platform.pickFiles(
|
||||||
@ -179,7 +196,7 @@ class _BottomSheetPickOption extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => Container(
|
Widget build(BuildContext context) => Container(
|
||||||
height: 300,
|
height: 255,
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
itemCount: options.length,
|
itemCount: options.length,
|
||||||
itemBuilder: (c, i) => ListTile(
|
itemBuilder: (c, i) => ListTile(
|
||||||
|
298
lib/ui/dialogs/record_audio_dialog.dart
Normal file
298
lib/ui/dialogs/record_audio_dialog.dart
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
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/ui_utils.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:record/record.dart';
|
||||||
|
import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
|
/// Record audio dialog
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
/// Record audio
|
||||||
|
Future<Uint8List> showRecordAudioDialog(BuildContext context) async {
|
||||||
|
if (!await Record.hasPermission()) {
|
||||||
|
await alert(
|
||||||
|
context, "Permission d'accéder au périphérique audio refusée !");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final res = await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (c) => Scaffold(
|
||||||
|
body: _RecordAudioDialog(),
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
),
|
||||||
|
barrierDismissible: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (await Record.isRecording()) await Record.stop();
|
||||||
|
|
||||||
|
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();
|
||||||
|
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: <Widget>[
|
||||||
|
_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: <Widget>[
|
||||||
|
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.m4a");
|
||||||
|
|
||||||
|
await Record.start(path: _recordPath);
|
||||||
|
|
||||||
|
setState(() => _recording = true);
|
||||||
|
} catch (e, s) {
|
||||||
|
logError(e, s);
|
||||||
|
snack(context, tr("Error while recording!"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _stopRecording() async {
|
||||||
|
try {
|
||||||
|
await Record.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,
|
||||||
|
this.visible,
|
||||||
|
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()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -49,5 +49,8 @@ Future<PickedFile> pickImage(BuildContext context) async {
|
|||||||
/// Check if a mime type maps to an image or not
|
/// Check if a mime type maps to an image or not
|
||||||
bool isImage(String mimeType) => mimeType.startsWith("image/");
|
bool isImage(String mimeType) => mimeType.startsWith("image/");
|
||||||
|
|
||||||
/// Check if a mime type maps to an image or not
|
/// Check if a mime type maps to a video or not
|
||||||
bool isVideo(String mimeType) => mimeType.startsWith("video/mp4");
|
bool isVideo(String mimeType) => mimeType.startsWith("video/mp4");
|
||||||
|
|
||||||
|
/// Check if a mime type maps to an audio file or not
|
||||||
|
bool isAudio(String mimeType) => mimeType.startsWith("audio/mpeg");
|
||||||
|
@ -83,6 +83,10 @@ void showSimpleSnack(BuildContext context, String message) {
|
|||||||
Scaffold.of(context).showSnackBar(SnackBar(content: Text(message)));
|
Scaffold.of(context).showSnackBar(SnackBar(content: Text(message)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void snack(BuildContext context, String message) {
|
||||||
|
Scaffold.of(context).showSnackBar(SnackBar(content: Text(message)));
|
||||||
|
}
|
||||||
|
|
||||||
/// Show an alert dialog to ask the user to enter a string
|
/// Show an alert dialog to ask the user to enter a string
|
||||||
///
|
///
|
||||||
/// Returns entered string if the dialog is confirmed, null else
|
/// Returns entered string if the dialog is confirmed, null else
|
||||||
|
@ -464,6 +464,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.1.0"
|
||||||
|
record:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: record
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.2"
|
||||||
rxdart:
|
rxdart:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -112,6 +112,9 @@ dependencies:
|
|||||||
# Create video thumbnails
|
# Create video thumbnails
|
||||||
video_thumbnail: ^0.2.5+1
|
video_thumbnail: ^0.2.5+1
|
||||||
|
|
||||||
|
# Record audio file
|
||||||
|
record: ^1.0.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
Loading…
Reference in New Issue
Block a user