// ignore_for_file: invalid_use_of_protected_member part of 'home_screen.dart'; extension _HomeScreenInteractions on _HomeScreenState { void _onHoldToSpeakCancel() { if (_isRecordingStarting) { _shouldStopWhenStartCompletes = false; _shouldCancelWhenStartCompletes = true; return; } _cancelRecording(showToast: false); } Future _cancelRecording({bool showToast = true}) async { try { await _voiceRecorder.stop(); _listeningAnimationController.stop(); } catch (_) {} if (!mounted) return; setState(() { _isRecording = false; _isCancelGestureActive = false; }); if (showToast) { Toast.show( context, context.l10n.homeRecordingCanceled, type: ToastType.info, ); } } Future _sendMessage( BuildContext context, { String? overrideContent, }) async { if (_isSendingMessage) { return; } final content = (overrideContent ?? _messageController.text).trim(); if (content.isEmpty && _selectedImages.isEmpty) return; final images = List.from(_selectedImages); final currentFocus = FocusManager.instance.primaryFocus; currentFocus?.unfocus(); _messageController.clear(); setState(() { _isSendingMessage = true; _selectedImages.clear(); }); try { await _chatBloc.sendMessage(content, images: images); } finally { if (mounted) { setState(() { _isSendingMessage = false; }); } } WidgetsBinding.instance.addPostFrameCallback((_) { if (_scrollController.hasClients) { _scrollController.animateTo( _scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: _scrollDurationMs), curve: Curves.easeOut, ); } }); } Future _onStopGenerating() async { final canceled = await _chatBloc.cancelCurrentRun(); if (!mounted) { return; } if (canceled) { Toast.show(context, context.l10n.homeStopRequested, type: ToastType.info); } } Future _startRecording() async { if (_isRecording || _isRecordingStarting) { return; } if (mounted) { setState(() { _isRecordingStarting = true; _shouldCancelWhenStartCompletes = false; _shouldStopWhenStartCompletes = false; }); } try { await _voiceRecorder.start(); _listeningAnimationController.repeat(); if (!mounted) { return; } if (_shouldStopWhenStartCompletes || _shouldCancelWhenStartCompletes) { final shouldCancelAfterStart = _shouldCancelWhenStartCompletes || _isCancelGestureActive; setState(() { _isRecordingStarting = false; _shouldCancelWhenStartCompletes = false; _shouldStopWhenStartCompletes = false; _isRecording = true; _isCancelGestureActive = false; }); if (shouldCancelAfterStart) { await _cancelRecording(showToast: false); return; } await _stopRecording(autoSendAfterTranscribe: true); return; } setState(() { _isRecordingStarting = false; _isRecording = true; _isCancelGestureActive = false; }); } catch (error) { if (!mounted) { return; } setState(() { _isRecordingStarting = false; _shouldCancelWhenStartCompletes = false; _shouldStopWhenStartCompletes = false; }); Toast.show(context, _readableError(error), type: ToastType.error); } } Future _stopRecording({bool autoSendAfterTranscribe = false}) async { String? audioPath; try { audioPath = await _voiceRecorder.stop(); _listeningAnimationController.stop(); if (!mounted) { return; } setState(() { _isRecording = false; _isTranscribing = true; _isCancelGestureActive = false; }); if (audioPath == null || audioPath.isEmpty) { throw StateError(context.l10n.errorGenericSafe); } final transcript = await _transcribeAudio(audioPath); if (!mounted) { return; } final normalizedTranscript = transcript.trim(); if (normalizedTranscript.isEmpty) { Toast.show( context, context.l10n.homeNoValidSpeech, type: ToastType.error, ); return; } _messageController.text = normalizedTranscript; _messageController.selection = TextSelection.fromPosition( TextPosition(offset: normalizedTranscript.length), ); if (autoSendAfterTranscribe) { setState(() { _isTranscribing = false; }); await _sendMessage(context); } } catch (error) { if (!mounted) { return; } Toast.show(context, _readableError(error), type: ToastType.error); } finally { try { if (audioPath != null) { final file = File(audioPath); if (await file.exists()) { await file.delete(); } } } catch (_) { // Ignore temp file cleanup errors to avoid blocking UI state recovery. } if (mounted) { setState(() { _isTranscribing = false; }); } } } String _readableError(Object error) { if (error is ApiException) { return error.message; } final raw = error.toString(); if (raw.startsWith('Instance of')) { return context.l10n.errorGenericSafe; } return raw.replaceFirst('Bad state: ', ''); } void _showBottomSheet(BuildContext context) { showModalBottomSheet( context: context, backgroundColor: Theme.of( context, ).colorScheme.surface.withValues(alpha: 0), isScrollControlled: true, builder: (context) => HomeSheet( onImagesSelected: (images) { setState(() { final remaining = 3 - _selectedImages.length; if (remaining > 0) { _selectedImages.addAll(images.take(remaining)); } }); }, ), ); } }