import 'dart:async'; import 'dart:developer'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/services/time.dart'; import 'package:island/widgets/content/sheet.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:path_provider/path_provider.dart'; import 'package:record/record.dart' hide Amplitude; import 'package:styled_widget/styled_widget.dart'; import 'package:uuid/uuid.dart'; import 'package:waveform_flutter/waveform_flutter.dart'; class ComposeRecorder extends HookConsumerWidget { const ComposeRecorder({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final recording = useState(false); final recordingStartAt = useState(null); final recordingDuration = useState(Duration(seconds: 0)); StreamSubscription? originalAmplitude; StreamController amplitudeStream = StreamController(); var record = AudioRecorder(); final resultPath = useState(null); Future startRecord() async { recording.value = true; // Check and request permission if needed final tempPath = !kIsWeb ? (await getTemporaryDirectory()).path : 'temp'; final uuid = const Uuid().v4().substring(0, 8); if (!await record.hasPermission()) return; const recordConfig = RecordConfig( encoder: AudioEncoder.pcm16bits, autoGain: true, echoCancel: true, noiseSuppress: true, ); resultPath.value = '$tempPath/solar-network-record-$uuid.m4a'; await record.start(recordConfig, path: resultPath.value!); recordingStartAt.value = DateTime.now(); originalAmplitude = record .onAmplitudeChanged(const Duration(milliseconds: 100)) .listen((value) async { amplitudeStream.add( Amplitude(current: value.current, max: value.max), ); recordingDuration.value = DateTime.now().difference( recordingStartAt.value!, ); }); } useEffect(() { return () { // Called when widget is unmounted log('[Recorder] Clean up!'); originalAmplitude?.cancel(); amplitudeStream.close(); record.dispose(); }; }, []); Future stopRecord() async { recording.value = false; await record.pause(); final newResult = await record.stop(); await record.cancel(); if (newResult != null) resultPath.value = newResult; if (context.mounted) Navigator.of(context).pop(resultPath.value); } return SheetScaffold( titleText: "recordAudio".tr(), child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ const Gap(32), Text( recordingDuration.value.formatShortDuration(), ).fontSize(20).bold().padding(bottom: 8), SizedBox( height: 120, child: Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 480), child: Card( color: Theme.of(context).colorScheme.surfaceContainer, child: AnimatedWaveList(stream: amplitudeStream.stream), ), ), ), ), const Gap(12), IconButton.filled( onPressed: recording.value ? stopRecord : startRecord, iconSize: 32, icon: recording.value ? const Icon(Symbols.stop, fill: 1, color: Colors.white) : const Icon( Symbols.play_arrow, fill: 1, color: Colors.white, ), ), ], ), ); } }