didvan-app/lib/views/widgets/ai_voice_chat_dialog.dart

1239 lines
42 KiB
Dart

// ignore_for_file: deprecated_member_use
import 'package:didvan/services/ai_rag_service.dart';
import 'package:didvan/services/ai_voice_service.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:record/record.dart';
import 'package:path_provider/path_provider.dart';
import 'package:just_audio/just_audio.dart';
import 'dart:ui';
import 'dart:io';
import 'dart:math' as math;
class AiVoiceChatDialog extends StatefulWidget {
const AiVoiceChatDialog({super.key});
@override
State<AiVoiceChatDialog> createState() => _AiVoiceChatDialogState();
}
class _AiVoiceChatDialogState extends State<AiVoiceChatDialog>
with TickerProviderStateMixin {
bool _isRecording = false;
bool _isPreparing = false;
bool _isProcessing = false;
bool _isAiSpeaking = false;
String _statusText = 'برای شروع مکالمه، دکمه میکروفون را نگه دارید';
String? _currentAiResponse;
late AnimationController _waveController;
late AnimationController _pulseController;
late AnimationController _glowController;
late AnimationController _particleController;
late AnimationController _preparingController;
final AudioRecorder _audioRecorder = AudioRecorder();
final AudioPlayer _audioPlayer = AudioPlayer();
String? _recordingPath;
final List<double> _audioWaveHeights = List.generate(40, (_) => 0.3);
int _currentWaveIndex = 0;
final List<String> _thinkingMessages = [
'در حال فکر کردن...',
'در حال بررسی اطلاعات...',
'در حال تحلیل سوال شما...',
'در حال جستجو در دانش...',
'در حال پردازش درخواست...',
'در حال یافتن بهترین پاسخ...',
'در حال بررسی جزئیات...',
'در حال تحلیل داده‌های مرتبط...',
'در حال مقایسه‌ی نتایج ممکن...',
'در حال جمع‌آوری اطلاعات موردنیاز...',
'در حال تشخیص الگوها...'
];
final List<String> _analyzingMessages = [
'در حال تجزیه و تحلیل...',
'در حال پردازش داده‌ها...',
'در حال بررسی محتوا...',
'در حال ترکیب اطلاعات...',
'در حال درک منظور شما...',
'در حال آماده‌سازی پاسخ...',
'در حال محاسبه‌ی بهترین گزینه...',
'در حال درک مفهوم پرسش شما...',
'در حال به‌روز‌رسانی اطلاعات...',
'در حال استخراج پاسخ مناسب...',
'در حال هماهنگ‌سازی با پایگاه دانش...'
];
int _currentThinkingIndex = 0;
int _currentAnalyzingIndex = 0;
@override
void initState() {
super.initState();
_waveController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 100),
)..addListener(() {
if (_isRecording || _isAiSpeaking) {
setState(() {
_currentWaveIndex =
(_currentWaveIndex + 1) % _audioWaveHeights.length;
for (int i = 0; i < _audioWaveHeights.length; i++) {
_audioWaveHeights[i] = 0.3 + (math.Random().nextDouble() * 0.7);
}
});
}
});
_pulseController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1500),
)..repeat(reverse: true);
_glowController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 2000),
)..repeat(reverse: true);
_particleController = AnimationController(
vsync: this,
duration: const Duration(seconds: 3),
)..repeat();
_preparingController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 800),
)..repeat(reverse: true);
_audioPlayer.playerStateStream.listen((state) {
if (mounted) {
setState(() {
_isAiSpeaking = state.playing;
if (state.processingState == ProcessingState.completed) {
_isAiSpeaking = false;
_statusText = 'برای ادامه مکالمه، دکمه میکروفون را نگه دارید';
_waveController.stop();
} else if (state.playing) {
_statusText = 'دستیار در حال پاسخ دادن است...';
_waveController.repeat();
}
});
}
});
}
@override
void dispose() {
_waveController.dispose();
_pulseController.dispose();
_glowController.dispose();
_particleController.dispose();
_preparingController.dispose();
_audioRecorder.dispose();
_audioPlayer.dispose();
super.dispose();
}
Future<void> _startRecording() async {
try {
if (await _audioRecorder.hasPermission()) {
if (_isAiSpeaking) {
await _audioPlayer.stop();
setState(() {
_isAiSpeaking = false;
});
}
// <<< راه‌حل مشکل ۲: ریست کردن پاسخ قبلی
if (_currentAiResponse != null) {
setState(() {
_currentAiResponse = null;
});
}
// >>>
setState(() {
_isPreparing = true;
_statusText = '⏳ در حال آماده سازی...';
});
_preparingController.repeat(reverse: true);
final directory = await getTemporaryDirectory();
final timestamp = DateTime.now().millisecondsSinceEpoch;
_recordingPath = '${directory.path}/voice_$timestamp.m4a';
await _audioRecorder.start(
const RecordConfig(
encoder: AudioEncoder.aacLc,
),
path: _recordingPath!,
);
await Future.delayed(const Duration(seconds: 2));
// <<< راه‌حل مشکل ۲: بررسی وضعیت قبل از شروع ضبط
if (mounted && _isPreparing) {
setState(() {
_isPreparing = false;
_isRecording = true;
_statusText = '🎙️ در حال گوش دادن...';
});
_preparingController.stop();
_waveController.repeat();
} else if (mounted) {
// اگر کاربر انگشت خود را برداشته بود (_isPreparing false شده)
await _audioRecorder.stop(); // ضبط را متوقف کن
_waveController.stop();
}
// >>>
}
} catch (e) {
setState(() {
_isPreparing = false;
_isRecording = false;
_statusText = 'خطا در شروع ضبط صدا';
});
// <<< راه‌حل مشکل ۲: توقف انیمیشن‌ها در صورت خطا
_preparingController.stop();
_waveController.stop();
// >>>
debugPrint('Error starting recording: $e');
}
}
Future<void> _stopRecording() async {
if (!_isRecording) return;
try {
setState(() {
_isRecording = false;
_isProcessing = true;
_currentThinkingIndex = 0;
_statusText = _thinkingMessages[_currentThinkingIndex];
});
_waveController.stop();
_startThinkingMessageRotation();
await Future.delayed(const Duration(seconds: 2));
if (!mounted) return;
final path = await _audioRecorder.stop();
if (path != null) {
final response = await AiVoiceService.uploadVoice(path);
if (response.isSuccess && response.text.isNotEmpty) {
setState(() {
_currentAnalyzingIndex = 0;
_statusText = _analyzingMessages[_currentAnalyzingIndex];
});
_startAnalyzingMessageRotation();
final ragResponse = await AiRagService.sendMessage(response.text);
if (ragResponse.audioUrl != null &&
ragResponse.audioUrl!.isNotEmpty) {
setState(() {
_statusText = '🔊 دستیار در حال پاسخ دادن است...';
_isProcessing = false;
_isAiSpeaking = true;
_currentAiResponse = ragResponse.output;
});
await _audioPlayer.setUrl(ragResponse.audioUrl!);
await _audioPlayer.play();
_waveController.repeat();
} else {
setState(() {
_statusText = 'خطا در پردازش';
_isProcessing = false;
});
}
} else {
setState(() {
_statusText = 'خطا در پردازش پیام صوتی';
_isProcessing = false;
});
}
try {
await File(path).delete();
} catch (e) {
debugPrint('Error deleting temp file: $e');
}
} else {
setState(() {
_isProcessing = false;
_statusText = 'خطا در ضبط صدا';
});
}
} catch (e) {
setState(() {
_isRecording = false;
_isProcessing = false;
_statusText = 'خطا در پردازش: ${e.toString()}';
});
_waveController.stop();
debugPrint('Error stopping recording: $e');
}
}
void _startThinkingMessageRotation() {
Future.delayed(const Duration(seconds: 10), () {
if (mounted && _isProcessing && !_isAiSpeaking) {
setState(() {
_currentThinkingIndex =
(_currentThinkingIndex + 1) % _thinkingMessages.length;
_statusText = _thinkingMessages[_currentThinkingIndex];
});
_startThinkingMessageRotation();
}
});
}
void _startAnalyzingMessageRotation() {
Future.delayed(const Duration(seconds: 10), () {
if (mounted && _isProcessing && !_isAiSpeaking) {
setState(() {
_currentAnalyzingIndex =
(_currentAnalyzingIndex + 1) % _analyzingMessages.length;
_statusText = _analyzingMessages[_currentAnalyzingIndex];
});
_startAnalyzingMessageRotation();
}
});
}
Color _getMainColor() {
if (_isPreparing) return const Color(0xFFFFA500);
if (_isRecording) return const Color.fromARGB(255, 178, 4, 54);
if (_isAiSpeaking) return const Color(0xFF00AAFF);
if (_isProcessing) return const Color(0xFFFFAA00);
return const Color(0xFF6B7280);
}
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: Colors.transparent,
insetPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 40),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
child: Container(
constraints: const BoxConstraints(maxWidth: 400, maxHeight: 700),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
const Color(0xFF0A0E27).withOpacity(0.95),
const Color(0xFF1A1F3A).withOpacity(0.95),
],
),
borderRadius: BorderRadius.circular(32),
border: Border.all(
color: Colors.white.withOpacity(0.1),
width: 1.5,
),
boxShadow: [
BoxShadow(
color: _getMainColor().withOpacity(0.3),
blurRadius: 40,
spreadRadius: 0,
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(32),
child: Stack(
children: [
_buildAnimatedBackground(),
Column(
children: [
_buildHeader(),
Expanded(
child: _buildMainContent(),
),
_buildControls(),
],
),
],
),
),
),
),
);
}
Widget _buildAnimatedBackground() {
return AnimatedBuilder(
animation: _particleController,
builder: (context, child) {
return CustomPaint(
painter: ParticlePainter(
animation: _particleController,
color: _getMainColor(),
),
size: Size.infinite,
);
},
);
}
Widget _buildHeader() {
return Container(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 16),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.white.withOpacity(0.05),
Colors.transparent,
],
),
),
child: Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [_getMainColor(), _getMainColor().withOpacity(0.6)],
),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: _getMainColor().withOpacity(0.5),
blurRadius: 20,
spreadRadius: 2,
),
],
),
child: Icon(
_isRecording
? Icons.mic_rounded
: _isAiSpeaking
? Icons.volume_up_rounded
: Icons.headset_mic_rounded,
color: Colors.white,
size: 24,
),
),
const SizedBox(width: 16),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DidvanText(
'گفتگوی صوتی',
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
SizedBox(height: 4),
DidvanText(
'دستیار هوشمند دیدوان',
fontSize: 12,
color: Colors.white60,
),
],
),
),
IconButton(
icon: const Icon(Icons.close_rounded, color: Colors.white70),
onPressed: () => Navigator.pop(context),
),
],
),
);
}
Widget _buildMainContent() {
return SingleChildScrollView(
child: Column(
children: [
const SizedBox(height: 20),
SizedBox(
height: 500,
child: Stack(
alignment: Alignment.topCenter,
children: [
Positioned(
top: 0,
child: _buildVisualization(),
),
// وقتی پاسخ هست، کارت پاسخ را نمایش می‌دهیم
if (_currentAiResponse != null && _currentAiResponse!.isNotEmpty)
Positioned(
bottom: 120,
left: 24,
right: 24,
child: _buildResponsePreview(),
)
else
// در غیر این صورت، متن status را نمایش می‌دهیم
Positioned(
bottom: 130,
left: 24,
right: 24,
child: _buildStatusText(),
),
],
),
),
const SizedBox(height: 20),
],
),
);
}
Widget _buildVisualization() {
return AnimatedBuilder(
animation: Listenable.merge(
[_pulseController, _glowController, _preparingController]),
builder: (context, child) {
final pulseValue = _pulseController.value;
final glowValue = _glowController.value;
final preparingValue = _preparingController.value;
return Stack(
alignment: Alignment.center,
children: [
if (_isPreparing) ...[
for (int i = 0; i < 4; i++)
Transform.scale(
scale: 1.0 + (preparingValue * 0.3) + (i * 0.15),
child: Container(
width: 200.0,
height: 200.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: const Color(0xFFFFA500)
.withOpacity(0.4 - (i * 0.1)),
width: 3,
),
),
),
),
],
if (!_isPreparing)
for (int i = 0; i < 3; i++)
Container(
width: 280 + (i * 40) + (pulseValue * 20),
height: 280 + (i * 40) + (pulseValue * 20),
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: _getMainColor().withOpacity(0.1 - (i * 0.03)),
width: 2,
),
),
),
Container(
width: 260 + (glowValue * 20),
height: 260 + (glowValue * 20),
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [
_getMainColor().withOpacity(0.3),
_getMainColor().withOpacity(0.1),
Colors.transparent,
],
),
),
),
if (_isPreparing)
SizedBox(
width: 240,
height: 240,
child: CustomPaint(
painter: PreparingSpinnerPainter(
animation: preparingValue,
color: const Color(0xFFFFA500),
),
),
)
else
SizedBox(
width: 240,
height: 240,
child: CustomPaint(
painter: WaveVisualizerPainter(
waveHeights: _audioWaveHeights,
color: _getMainColor(),
isActive: _isRecording || _isAiSpeaking,
),
),
),
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
_getMainColor(),
_getMainColor().withOpacity(0.7),
],
),
boxShadow: [
BoxShadow(
color: _getMainColor().withOpacity(0.6),
blurRadius: 30,
spreadRadius: 5,
),
],
),
child: Icon(
_isPreparing
? Icons.settings_rounded
: _isRecording
? Icons.mic_rounded
: _isAiSpeaking
? Icons.graphic_eq_rounded
: _isProcessing
? Icons.hourglass_empty_rounded
: Icons.headphones_rounded,
color: Colors.white,
size: 50,
),
),
],
);
},
);
}
Widget _buildStatusText() {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 32),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.05),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: _getMainColor().withOpacity(0.3),
width: 1,
),
),
child: DidvanText(
_statusText,
fontSize: 14,
color: Colors.white,
textAlign: TextAlign.center,
),
);
}
Widget _buildResponsePreview() {
return TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: const Duration(milliseconds: 600),
curve: Curves.easeOutCubic,
builder: (context, value, child) {
return Transform.translate(
offset: Offset(0, 30 * (1 - value)),
child: Opacity(
opacity: value,
child: child,
),
);
},
child: Container(
margin: const EdgeInsets.fromLTRB(2, 8, 2, 0),
child: ClipRRect(
borderRadius: BorderRadius.circular(24),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15),
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.white.withOpacity(0.15),
Colors.white.withOpacity(0.08),
],
),
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: Colors.white.withOpacity(0.2),
width: 1.5,
),
boxShadow: [
BoxShadow(
color: const Color(0xFF00AAFF).withOpacity(0.2),
blurRadius: 20,
spreadRadius: 0,
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: _showFullResponseDialog,
borderRadius: BorderRadius.circular(24),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [
Color(0xFF00AAFF),
Color(0xFF0066AA),
],
),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: const Color(0xFF00AAFF).withOpacity(0.4),
blurRadius: 12,
spreadRadius: 2,
),
],
),
child: const Icon(
Icons.chat_bubble_rounded,
color: Colors.white,
size: 20,
),
),
const SizedBox(width: 12),
const Expanded(
child: DidvanText(
'پاسخ دستیار',
fontSize: 13,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.white.withOpacity(0.2),
Colors.white.withOpacity(0.1),
],
),
borderRadius: BorderRadius.circular(12),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
DidvanText(
'مشاهده کامل',
fontSize: 11,
color: Colors.white,
fontWeight: FontWeight.w600,
),
SizedBox(width: 4),
Icon(
Icons.arrow_back_ios_rounded,
color: Colors.white,
size: 12,
),
],
),
),
],
),
const SizedBox(height: 16),
DidvanText(
_currentAiResponse!,
fontSize: 13.5,
color: Colors.white.withOpacity(0.95),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
3,
(index) => Container(
margin: const EdgeInsets.symmetric(horizontal: 2),
width: 4,
height: 4,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.6),
shape: BoxShape.circle,
),
),
),
),
],
),
),
),
),
),
),
),
),
);
}
void _showFullResponseDialog() {
showDialog(
context: context,
barrierColor: Colors.black.withOpacity(0.7),
builder: (context) => Dialog(
backgroundColor: Colors.transparent,
insetPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 40),
child: ScaleTransition(
scale: CurvedAnimation(
parent: AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
)..forward(),
curve: Curves.easeOutBack,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(28),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
child: Container(
constraints: const BoxConstraints(maxHeight: 600),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
const Color(0xFF0A0E27).withOpacity(0.95),
const Color(0xFF1A1F3A).withOpacity(0.95),
],
),
borderRadius: BorderRadius.circular(28),
border: Border.all(
color: Colors.white.withOpacity(0.15),
width: 1.5,
),
boxShadow: [
BoxShadow(
color: const Color(0xFF00AAFF).withOpacity(0.3),
blurRadius: 40,
spreadRadius: 0,
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.white.withOpacity(0.08),
Colors.transparent,
],
),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [
Color(0xFF00AAFF),
Color(0xFF0066AA),
],
),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: const Color(0xFF00AAFF).withOpacity(0.4),
blurRadius: 15,
spreadRadius: 2,
),
],
),
child: const Icon(
Icons.chat_bubble_rounded,
color: Colors.white,
size: 24,
),
),
const SizedBox(width: 14),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DidvanText(
'پاسخ کامل دستیار',
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
SizedBox(height: 2),
DidvanText(
'دیدوان AI',
fontSize: 11,
color: Colors.white60,
),
],
),
),
IconButton(
icon: const Icon(
Icons.close_rounded,
color: Colors.white70,
size: 24,
),
onPressed: () => Navigator.pop(context),
),
],
),
),
Flexible(
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(24, 8, 24, 24),
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.05),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Colors.white.withOpacity(0.1),
width: 1,
),
),
child: DidvanText(
_currentAiResponse ?? '',
fontSize: 14,
color: Colors.white.withOpacity(0.95),
),
),
),
),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Colors.white.withOpacity(0.05),
Colors.transparent,
],
),
),
child: Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () {
Clipboard.setData(ClipboardData(text: _currentAiResponse ?? ''));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const DidvanText(
'متن کپی شد',
color: Colors.white,
fontSize: 13,
),
backgroundColor: const Color(0xFF00AAFF),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
duration: const Duration(seconds: 2),
),
);
},
icon: const Icon(
Icons.copy_rounded,
size: 18,
color: Colors.white,
),
label: const DidvanText(
'کپی متن',
fontSize: 13,
color: Colors.white,
fontWeight: FontWeight.w600,
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white.withOpacity(0.15),
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 14,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(
color: Colors.white.withOpacity(0.2),
width: 1,
),
),
elevation: 0,
),
),
),
],
),
),
],
),
),
),
),
),
),
);
}
Widget _buildControls() {
return Container(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (_isAiSpeaking)
const Padding(
padding: EdgeInsets.only(bottom: 16),
child: DidvanText(
'',
fontSize: 12,
color: Colors.white70,
textAlign: TextAlign.center,
),
),
GestureDetector(
onLongPressStart: (_) {
if (!_isProcessing && !_isRecording && !_isPreparing) {
_startRecording();
}
},
// <<< راه‌حل مشکل ۲: (بدون تغییر، همچنان پابرجاست)
onLongPressEnd: (_) {
if (_isRecording) {
_stopRecording();
} else if (_isPreparing) {
// کاربر انگشت خود را در حین آماده‌سازی برداشت
setState(() {
_isPreparing = false;
_statusText = 'برای شروع مکالمه، دکمه میکروفون را نگه دارید';
});
_preparingController.stop();
_waveController.stop();
}
},
// >>>
child: AnimatedBuilder(
animation: _pulseController,
builder: (context, child) {
return Container(
width: 80,
height: 80,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: _isPreparing
? [const Color(0xFFFFA500), const Color(0xFFFFCC00)]
: _isRecording
? [
const Color.fromARGB(255, 178, 4, 54),
const Color.fromARGB(255, 255, 200, 215)
]
: _isProcessing
? [Colors.grey.shade600, Colors.grey.shade700]
: [
const Color(0xFF0066AA),
const Color(0xFF00AAFF)
],
),
boxShadow: [
BoxShadow(
color: _getMainColor().withOpacity(0.5),
blurRadius: 20 + (_pulseController.value * 10),
spreadRadius: 5 + (_pulseController.value * 5),
),
],
),
child: Icon(
_isPreparing || _isRecording
? Icons.stop_rounded
: Icons.mic_rounded,
color: Colors.white,
size: 36,
),
);
},
),
),
],
),
);
}
}
class WaveVisualizerPainter extends CustomPainter {
final List<double> waveHeights;
final Color color;
final bool isActive;
WaveVisualizerPainter({
required this.waveHeights,
required this.color,
required this.isActive,
});
@override
void paint(Canvas canvas, Size size) {
if (!isActive) return;
final paint = Paint()
..color = color
..style = PaintingStyle.fill;
final center = Offset(size.width / 2, size.height / 2);
final radius = size.width / 2;
final barCount = waveHeights.length;
final angleStep = (2 * math.pi) / barCount;
for (int i = 0; i < barCount; i++) {
final angle = i * angleStep;
final height = waveHeights[i] * 40;
final startX = center.dx + (radius - 10) * math.cos(angle);
final startY = center.dy + (radius - 10) * math.sin(angle);
final endX = center.dx + (radius - 10 + height) * math.cos(angle);
final endY = center.dy + (radius - 10 + height) * math.sin(angle);
paint.strokeWidth = 3;
paint.strokeCap = StrokeCap.round;
paint.color = color.withOpacity(0.6 + (waveHeights[i] * 0.4));
canvas.drawLine(
Offset(startX, startY),
Offset(endX, endY),
paint,
);
}
}
@override
bool shouldRepaint(covariant WaveVisualizerPainter oldDelegate) {
return true;
}
}
class ParticlePainter extends CustomPainter {
final Animation<double> animation;
final Color color;
ParticlePainter({required this.animation, required this.color});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color.withOpacity(0.1)
..style = PaintingStyle.fill;
final random = math.Random(42);
for (int i = 0; i < 30; i++) {
final x = random.nextDouble() * size.width;
final baseY = random.nextDouble() * size.height;
final y = baseY + (animation.value * 100) % size.height;
final radius = 1 + random.nextDouble() * 2;
canvas.drawCircle(Offset(x, y), radius, paint);
}
}
@override
bool shouldRepaint(covariant ParticlePainter oldDelegate) {
return true;
}
}
class PreparingSpinnerPainter extends CustomPainter {
final double animation;
final Color color;
PreparingSpinnerPainter({required this.animation, required this.color});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 4
..strokeCap = StrokeCap.round;
final center = Offset(size.width / 2, size.height / 2);
final radius = size.width / 2 - 20;
const dotCount = 12;
const angleStep = (2 * math.pi) / dotCount;
for (int i = 0; i < dotCount; i++) {
final angle = (i * angleStep) + (animation * 2 * math.pi);
final opacity = (1.0 - (i / dotCount)) * 0.8;
final x = center.dx + radius * math.cos(angle);
final y = center.dy + radius * math.sin(angle);
paint.color = color.withOpacity(opacity);
canvas.drawCircle(
Offset(x, y),
3 + (animation * 2),
paint..style = PaintingStyle.fill,
);
}
for (int i = 0; i < 3; i++) {
final arcRadius = radius - (i * 25);
final startAngle = (animation * 2 * math.pi) + (i * math.pi / 3);
final sweepAngle = math.pi / 2 + (animation * math.pi / 4);
paint
..style = PaintingStyle.stroke
..strokeWidth = 3.0 - i
..color = color.withOpacity(0.5 - (i * 0.1));
canvas.drawArc(
Rect.fromCircle(center: center, radius: arcRadius),
startAngle,
sweepAngle,
false,
paint,
);
}
}
@override
bool shouldRepaint(covariant PreparingSpinnerPainter oldDelegate) {
return true;
}
}