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 createState() => _RecorderState(); } class _RecorderState extends State { final recorder = FlutterSoundRecorder(); bool isRecorderReady = false; String? path; Timer? timer; ValueNotifier 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( 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(); } }