189 lines
5.4 KiB
Dart
189 lines
5.4 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:cross_file/cross_file.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_sound/flutter_sound.dart';
|
|
import 'package:flutter_spinkit/flutter_spinkit.dart';
|
|
import 'package:hoshan/core/gen/assets.gen.dart';
|
|
import 'package:hoshan/core/services/permission/permission_service.dart';
|
|
import 'package:hoshan/core/utils/date_time.dart';
|
|
import 'package:hoshan/ui/theme/colors.dart';
|
|
import 'package:hoshan/ui/theme/text.dart';
|
|
import 'package:hoshan/ui/widgets/components/audio/player.dart';
|
|
import 'package:hoshan/ui/widgets/components/button/circle_icon_btn.dart';
|
|
import 'package:path_provider/path_provider.dart';
|
|
import 'package:permission_handler/permission_handler.dart';
|
|
|
|
class Recorder extends StatefulWidget {
|
|
final bool play;
|
|
final Function(XFile) onRecordFinish;
|
|
final Function()? onDelete;
|
|
final Function(String?)? onError;
|
|
const Recorder(
|
|
{super.key,
|
|
required this.play,
|
|
this.onDelete,
|
|
required this.onRecordFinish,
|
|
this.onError});
|
|
|
|
@override
|
|
State<Recorder> createState() => _RecorderState();
|
|
}
|
|
|
|
class _RecorderState extends State<Recorder> {
|
|
final recorder = FlutterSoundRecorder();
|
|
bool isRecorderReady = false;
|
|
String? path;
|
|
Timer? timer;
|
|
ValueNotifier<int> seconds = ValueNotifier(0);
|
|
|
|
@override
|
|
void initState() {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
|
await initRecorder();
|
|
});
|
|
super.initState();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
super.dispose();
|
|
timer?.cancel();
|
|
}
|
|
|
|
Future initRecorder() async {
|
|
final status = await PermissionService.getPermission(
|
|
permission: Permission.microphone);
|
|
if (!status) {
|
|
widget.onError?.call('Permission Error');
|
|
throw 'Permission Error';
|
|
}
|
|
|
|
await recorder.openRecorder();
|
|
setState(() {
|
|
isRecorderReady = true;
|
|
});
|
|
recorder.setSubscriptionDuration(const Duration(milliseconds: 500));
|
|
if (widget.play) {
|
|
await record();
|
|
}
|
|
}
|
|
|
|
Future record() async {
|
|
if (!isRecorderReady) return;
|
|
try {
|
|
final tempDir = await getTemporaryDirectory();
|
|
final fileName = '${DateTime.now().millisecondsSinceEpoch ~/ 1000}.aac';
|
|
final filePath = '${tempDir.path}/$fileName';
|
|
|
|
if (kDebugMode) {
|
|
print('Starting recording to: $filePath');
|
|
}
|
|
|
|
await recorder.startRecorder(
|
|
toFile: filePath,
|
|
codec: Codec.aacMP4,
|
|
);
|
|
timer = Timer.periodic(
|
|
const Duration(seconds: 1),
|
|
(timer) {
|
|
seconds.value = seconds.value + 1;
|
|
},
|
|
);
|
|
} catch (e) {
|
|
widget.onError?.call('$e');
|
|
|
|
if (kDebugMode) {
|
|
print('record Error: $e');
|
|
}
|
|
}
|
|
}
|
|
|
|
Future recordStop() async {
|
|
if (!isRecorderReady) return;
|
|
timer?.cancel();
|
|
path = await recorder.stopRecorder();
|
|
if (path == null) {
|
|
widget.onError?.call('record File Path Error');
|
|
|
|
throw 'record File Path Error';
|
|
}
|
|
setState(() {});
|
|
final XFile file = XFile(path!);
|
|
widget.onRecordFinish(file);
|
|
if (kDebugMode) {
|
|
print("filePath: $path");
|
|
}
|
|
}
|
|
|
|
void handleRecorder() async {
|
|
if (recorder.isRecording) {
|
|
await recordStop();
|
|
} else {
|
|
await record();
|
|
}
|
|
setState(() {});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return isRecorderReady
|
|
? SizedBox(
|
|
height: 46,
|
|
child: path != null
|
|
? Row(
|
|
children: [
|
|
Expanded(
|
|
child: Player(
|
|
fileUrl: path!,
|
|
onDelete: widget.onDelete,
|
|
)),
|
|
],
|
|
)
|
|
: Row(
|
|
children: [
|
|
CircleIconBtn(
|
|
icon: Assets.icon.bold.stop,
|
|
onTap: handleRecorder,
|
|
),
|
|
const SizedBox(
|
|
width: 8,
|
|
),
|
|
Expanded(
|
|
child: SizedBox(
|
|
height: 28,
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: List.generate(
|
|
6,
|
|
(index) => SpinKitWave(
|
|
color: AppColors.primaryColor.defaultShade,
|
|
size: 32,
|
|
itemCount: 10,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(
|
|
width: 8,
|
|
),
|
|
ValueListenableBuilder<int>(
|
|
valueListenable: seconds,
|
|
builder: (context, s, _) {
|
|
return Text(
|
|
DateTimeUtils.getTimeFromDuration(s),
|
|
style: AppTextStyles.body4.copyWith(
|
|
color: Theme.of(context).colorScheme.onSurface,
|
|
fontWeight: FontWeight.bold),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
)
|
|
: const SizedBox.shrink();
|
|
}
|
|
}
|