2022-11-01 08:23:06 +01:00
|
|
|
// SPDX-FileCopyrightText: 2022 Iconica
|
|
|
|
//
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
|
2022-10-25 14:20:18 +02:00
|
|
|
import 'dart:async';
|
|
|
|
import 'dart:io';
|
|
|
|
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:flutter_media_picker/flutter_media_picker.dart';
|
2022-12-23 11:57:28 +01:00
|
|
|
import 'package:flutter_media_picker/src/widgets/icon_button_with_text.dart';
|
2022-10-25 14:20:18 +02:00
|
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
|
|
import 'package:intl/intl.dart';
|
|
|
|
|
|
|
|
/// Input for audio used by [MediaPicker].
|
|
|
|
///
|
2022-12-23 11:57:28 +01:00
|
|
|
/// This feature is only usable for native applications.
|
2022-10-25 14:20:18 +02:00
|
|
|
class MediaPickerInputAudio implements MediaPickerInput {
|
|
|
|
MediaPickerInputAudio({
|
|
|
|
this.label = "Audio",
|
|
|
|
this.checkPageSettings,
|
|
|
|
this.onComplete,
|
|
|
|
required this.audioService,
|
2022-10-27 11:21:24 +02:00
|
|
|
this.inputStyling,
|
2022-12-23 11:57:28 +01:00
|
|
|
}) : icon = IconButtonWithText(
|
|
|
|
icon: Icons.record_voice_over,
|
|
|
|
iconText: label,
|
|
|
|
);
|
2022-10-25 14:20:18 +02:00
|
|
|
|
|
|
|
final AudioService audioService;
|
|
|
|
|
2022-10-27 11:21:24 +02:00
|
|
|
final AudioInputStyling? inputStyling;
|
|
|
|
|
2022-10-25 14:20:18 +02:00
|
|
|
@override
|
|
|
|
String label;
|
|
|
|
|
2022-12-22 16:18:33 +01:00
|
|
|
@override
|
|
|
|
Widget icon;
|
|
|
|
|
2022-10-25 14:20:18 +02:00
|
|
|
@override
|
|
|
|
Future<MediaResult> onPressed(BuildContext context) async {
|
|
|
|
MediaResult audio = MediaResult();
|
|
|
|
|
|
|
|
await Navigator.push(
|
|
|
|
context,
|
|
|
|
MaterialPageRoute(
|
|
|
|
builder: (context) => Recorder(
|
|
|
|
audioService: audioService,
|
|
|
|
onComplete: (MediaResult content) {
|
|
|
|
if (content.fileValue != null) {
|
|
|
|
audio = content;
|
|
|
|
} else {
|
|
|
|
throw Exception("No recording returned");
|
|
|
|
}
|
|
|
|
},
|
2022-10-27 11:21:24 +02:00
|
|
|
inputStyling: inputStyling ?? AudioInputStyling(),
|
2022-10-25 14:20:18 +02:00
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
|
|
|
|
return audio;
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<Widget> displayResult(MediaResult result) async {
|
|
|
|
var data = result.fileValue;
|
|
|
|
if (data != null) {
|
|
|
|
audioService.playAudio(data);
|
|
|
|
}
|
|
|
|
|
|
|
|
return Container();
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Map<String, dynamic>? checkPageSettings;
|
|
|
|
|
|
|
|
@override
|
|
|
|
void Function(MediaResult value)? onComplete;
|
|
|
|
}
|
|
|
|
|
|
|
|
class Recorder extends ConsumerStatefulWidget {
|
|
|
|
const Recorder({
|
|
|
|
required this.onComplete,
|
|
|
|
required this.audioService,
|
2022-10-27 11:21:24 +02:00
|
|
|
required this.inputStyling,
|
2022-10-25 14:20:18 +02:00
|
|
|
Key? key,
|
|
|
|
}) : super(key: key);
|
|
|
|
|
|
|
|
final void Function(MediaResult value) onComplete;
|
|
|
|
final AudioService audioService;
|
|
|
|
|
2022-10-27 11:21:24 +02:00
|
|
|
final AudioInputStyling inputStyling;
|
|
|
|
|
2022-10-25 14:20:18 +02:00
|
|
|
@override
|
|
|
|
ConsumerState<Recorder> createState() => _RecorderState();
|
|
|
|
}
|
|
|
|
|
|
|
|
class _RecorderState extends ConsumerState<Recorder> {
|
|
|
|
final Clock clock = Clock();
|
|
|
|
String? directory;
|
|
|
|
|
|
|
|
bool recording = false;
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
return Scaffold(
|
|
|
|
body: Stack(
|
|
|
|
children: [
|
2022-10-27 11:21:24 +02:00
|
|
|
widget.inputStyling.background ??
|
|
|
|
Container(
|
|
|
|
decoration: const BoxDecoration(
|
|
|
|
gradient: RadialGradient(
|
|
|
|
radius: 2,
|
|
|
|
colors: [
|
|
|
|
Color(0xFFFFFFFF),
|
|
|
|
Color(0xFFCCCCCC),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
),
|
2022-10-25 14:20:18 +02:00
|
|
|
),
|
|
|
|
Center(
|
|
|
|
child: FutureBuilder<String>(
|
|
|
|
future: widget.audioService.setWorkingDirectory(),
|
|
|
|
builder: (context, snapshot) {
|
|
|
|
if (snapshot.hasData) {
|
|
|
|
directory = snapshot.data;
|
|
|
|
return Column(
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
|
|
children: [
|
2022-10-27 11:21:24 +02:00
|
|
|
if (widget.inputStyling.pageContent != null)
|
|
|
|
widget.inputStyling.pageContent!,
|
2022-10-25 14:20:18 +02:00
|
|
|
StreamBuilder(
|
|
|
|
stream: Stream.periodic(
|
|
|
|
recording
|
|
|
|
? const Duration(milliseconds: 1000)
|
|
|
|
: const Duration(hours: 1),
|
|
|
|
),
|
|
|
|
builder: (context, snapshot) {
|
|
|
|
return Text(
|
|
|
|
DateFormat('mm:ss').format(
|
|
|
|
DateTime.fromMillisecondsSinceEpoch(
|
|
|
|
clock.getCurrentTime(),
|
|
|
|
),
|
|
|
|
),
|
2022-10-27 11:21:24 +02:00
|
|
|
style: widget.inputStyling.timeTextStyle ??
|
|
|
|
Theme.of(context).textTheme.headline5,
|
2022-10-25 14:20:18 +02:00
|
|
|
);
|
|
|
|
},
|
|
|
|
),
|
|
|
|
const SizedBox(
|
|
|
|
height: 15,
|
|
|
|
),
|
|
|
|
Row(
|
|
|
|
children: [
|
|
|
|
const Spacer(),
|
2022-10-27 11:21:24 +02:00
|
|
|
widget.inputStyling.playButton?.call(
|
|
|
|
recording,
|
|
|
|
playOnTap,
|
|
|
|
) ??
|
|
|
|
SizedBox(
|
|
|
|
width: 82,
|
|
|
|
height: 82,
|
|
|
|
child: Stack(
|
|
|
|
children: [
|
|
|
|
Center(
|
2022-10-25 14:20:18 +02:00
|
|
|
child: Container(
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
borderRadius:
|
|
|
|
BorderRadius.circular(45),
|
|
|
|
gradient: const LinearGradient(
|
|
|
|
colors: [
|
|
|
|
Color(0xFFF0F0F0),
|
2022-10-27 11:21:24 +02:00
|
|
|
Color(0xFFC6C6C6),
|
2022-10-25 14:20:18 +02:00
|
|
|
],
|
|
|
|
begin: Alignment.topLeft,
|
|
|
|
end: Alignment.bottomRight,
|
|
|
|
),
|
|
|
|
),
|
2022-10-27 11:21:24 +02:00
|
|
|
child: Center(
|
|
|
|
child: Container(
|
|
|
|
width: 60,
|
|
|
|
height: 60,
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
borderRadius:
|
|
|
|
BorderRadius.circular(45),
|
|
|
|
gradient: const LinearGradient(
|
|
|
|
colors: [
|
|
|
|
Color(0xFFC6C6C6),
|
|
|
|
Color(0xFFF0F0F0),
|
|
|
|
],
|
|
|
|
begin: Alignment.topLeft,
|
|
|
|
end: Alignment.bottomRight,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
2022-10-25 14:20:18 +02:00
|
|
|
),
|
|
|
|
),
|
2022-10-27 11:21:24 +02:00
|
|
|
Center(
|
|
|
|
child: IconButton(
|
|
|
|
iconSize: 65,
|
|
|
|
padding: EdgeInsets.zero,
|
|
|
|
splashRadius: 30,
|
|
|
|
onPressed: () {
|
|
|
|
playOnTap();
|
|
|
|
},
|
|
|
|
icon: Icon(
|
|
|
|
recording
|
|
|
|
? Icons.pause
|
|
|
|
: Icons.play_arrow_rounded,
|
|
|
|
color: const Color(0xFF4C4C4C),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
],
|
2022-10-25 14:20:18 +02:00
|
|
|
),
|
2022-10-27 11:21:24 +02:00
|
|
|
),
|
|
|
|
widget.inputStyling.nextButton != null
|
|
|
|
? Expanded(
|
|
|
|
child: Center(
|
|
|
|
child: widget.inputStyling.nextButton!.call(
|
|
|
|
recording,
|
|
|
|
nextOnTap,
|
2022-10-25 14:20:18 +02:00
|
|
|
),
|
|
|
|
),
|
|
|
|
)
|
2022-10-27 11:21:24 +02:00
|
|
|
: Expanded(
|
2022-10-25 14:20:18 +02:00
|
|
|
child: Center(
|
2022-10-27 11:21:24 +02:00
|
|
|
child: GestureDetector(
|
|
|
|
onTap: () async {
|
|
|
|
nextOnTap();
|
|
|
|
},
|
|
|
|
child: Container(
|
|
|
|
width:
|
|
|
|
MediaQuery.of(context).size.width *
|
|
|
|
0.3,
|
|
|
|
height: 45,
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
color: const Color(0xFFD8D8D8),
|
|
|
|
borderRadius:
|
|
|
|
BorderRadius.circular(15),
|
|
|
|
),
|
|
|
|
child: Center(
|
|
|
|
child: Text(
|
|
|
|
'Next',
|
|
|
|
style: Theme.of(context)
|
|
|
|
.textTheme
|
|
|
|
.button,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
2022-10-25 14:20:18 +02:00
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
const Spacer(
|
|
|
|
flex: 1,
|
|
|
|
),
|
|
|
|
],
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
return const CircularProgressIndicator();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
2022-10-27 11:21:24 +02:00
|
|
|
|
|
|
|
playOnTap() {
|
|
|
|
if (recording) {
|
|
|
|
widget.audioService.recordStop();
|
|
|
|
|
|
|
|
clock.stopClock();
|
|
|
|
|
|
|
|
setState(() {
|
|
|
|
recording = false;
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
widget.audioService.recordStart();
|
|
|
|
|
|
|
|
clock.startClock();
|
|
|
|
|
|
|
|
setState(() {
|
|
|
|
recording = true;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
nextOnTap() async {
|
|
|
|
widget.audioService.recordStop();
|
|
|
|
|
|
|
|
widget.onComplete(
|
|
|
|
MediaResult(
|
|
|
|
fileValue: await File(directory!).readAsBytes(),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
|
|
|
|
// ignore: use_build_context_synchronously
|
|
|
|
Navigator.pop(context);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Used by [MediaPickerInputAudio] to set styling options.
|
|
|
|
///
|
|
|
|
/// background can be set to determine the background of the page.
|
|
|
|
///
|
|
|
|
/// pageContent can be set to set any widget at the top of the page.
|
|
|
|
///
|
|
|
|
/// timeTextStyle sets the [TextStyle] of the time that shows the recording duration. Defaults to headline5.
|
|
|
|
///
|
|
|
|
/// playButton changes the default play/pause button.
|
|
|
|
///
|
|
|
|
/// nextButton changes the default next/finish button.
|
|
|
|
class AudioInputStyling {
|
|
|
|
AudioInputStyling({
|
|
|
|
this.background,
|
|
|
|
this.pageContent,
|
|
|
|
this.timeTextStyle,
|
|
|
|
this.playButton,
|
|
|
|
this.nextButton,
|
|
|
|
});
|
|
|
|
|
|
|
|
/// background can be set to determine the background of the page.
|
|
|
|
final Widget? background;
|
|
|
|
|
|
|
|
/// pageContent can be set to set any widget at the top of the page.
|
|
|
|
final Widget? pageContent;
|
|
|
|
|
|
|
|
/// timeTextStyle sets the [TextStyle] of the time that shows the recording duration. Defaults to headline5.
|
|
|
|
final TextStyle? timeTextStyle;
|
|
|
|
|
|
|
|
/// playButton changes the default play/pause button.
|
|
|
|
final Widget Function(bool recording, Function onTap)? playButton;
|
|
|
|
|
|
|
|
/// nextButton changes the default next/finish button.
|
|
|
|
final Widget Function(bool recording, Function onTap)? nextButton;
|
2022-10-25 14:20:18 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Generic clock class can be created and used to keep the time.
|
|
|
|
class Clock {
|
|
|
|
/// [startTime] indiciates the starting time of the clock
|
|
|
|
DateTime startTime = DateTime.now();
|
|
|
|
|
|
|
|
/// [endTime] indicates the end time of the clock
|
|
|
|
DateTime? endTime = DateTime.now();
|
|
|
|
|
|
|
|
int _millisecondsfromEpoch = 0;
|
|
|
|
|
|
|
|
/// [startClock] can be called to start the clock and count up from [startTime]
|
|
|
|
void startClock() {
|
|
|
|
startTime = DateTime.now();
|
|
|
|
endTime = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// [stopClock] can be called to stop the clock and saves the difference from [startTime] and [endTime]
|
|
|
|
void stopClock() {
|
|
|
|
endTime = DateTime.now();
|
|
|
|
_millisecondsfromEpoch +=
|
|
|
|
endTime!.millisecondsSinceEpoch - startTime.millisecondsSinceEpoch;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// [getCurrentTime] can be called to get the difference between [startTime] and [endTime] in milliseconds
|
|
|
|
int getCurrentTime() {
|
|
|
|
if (endTime != null) {
|
|
|
|
return _millisecondsfromEpoch;
|
|
|
|
} else {
|
|
|
|
return DateTime.now().millisecondsSinceEpoch -
|
|
|
|
startTime.millisecondsSinceEpoch +
|
|
|
|
_millisecondsfromEpoch;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|