Houshan-Basa/lib/ui/widgets/components/audio/recorder.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();
}
}