Conv
Conv
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:audioplayers/audioplayers.dart' as AudioPlayers;
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:record/record.dart' as Record;
class Message {
final int id;
final String? content;
final String? voiceUrl;
final int? duration;
final bool isOutgoing;
final DateTime date;
final bool isVoice;
final List<double>? waveformData;
Message({
required this.id,
this.content,
this.voiceUrl,
this.duration,
required this.isOutgoing,
required this.date,
required this.isVoice,
this.waveformData,
});
String? voiceUrl =
json['content']['voice_note']['voice']['remote']['url'];
if (voiceUrl == null || voiceUrl.isEmpty) {
final remoteId = json['content']['voice_note']['voice']['remote']['id'];
if (remoteId != null) {
voiceUrl = 'http://192.168.1.3:8000/files/voice_${remoteId}.wav';
}
}
return Message(
id: json['id'] ?? 0,
content: '[']پیام صوتی,
voiceUrl: voiceUrl,
duration: json['content']['voice_note']['duration'],
isOutgoing: json['is_outgoing'] ?? false,
date: json['date'] != null
? DateTime.fromMillisecondsSinceEpoch(json['date'] * 1000)
: DateTime.now(),
isVoice: true,
waveformData: parsedWaveformData,
);
}
return Message(
id: json['id'] ?? 0,
content: 'نشده
'محتوای پشتیبانی,
isOutgoing: json['is_outgoing'] ?? false,
date: DateTime.now(),
isVoice: false,
);
}
}
const ConversationScreen({
required this.chatId,
required this.chatTitle,
required this.phoneNumber,
super.key,
});
@override
State<ConversationScreen> createState() => _ConversationScreenState();
}
class _ConversationScreenState extends State<ConversationScreen> {
List<Message> messages = [];
String? errorMessage;
bool isLoading = true;
bool isLoadingMore = false;
int? oldestMessageId;
final ScrollController _scrollController = ScrollController();
final TextEditingController _messageController = TextEditingController();
final AudioPlayers.AudioPlayer _audioPlayer = AudioPlayers.AudioPlayer();
final Record.AudioRecorder _recorder = Record.AudioRecorder();
bool _isRecording = false;
bool _isPlaying = false;
bool _isAudioLoading = false;
String? _recordedFilePath;
int? _recordingDuration;
List<double>? _waveformData;
bool _isWaveformLoading = false;
Duration _audioPosition = Duration.zero;
Duration _audioDuration = Duration.zero;
String? _currentPlayingUrl;
StreamSubscription<AudioPlayers.PlayerState>? _playerStateSubscription;
StreamSubscription<Duration>? _positionSubscription;
StreamSubscription<Duration>? _durationSubscription;
Timer? _recordingTimer;
Timer? _pollTimer;
double? _lastScrollPosition;
bool _isAtBottom = true;
@override
void initState() {
super.initState();
_fetchMessages();
_scrollController.addListener(_onScroll);
_setupAudioPlayer();
_pollTimer = Timer.periodic(Duration(seconds: 5), (_) {
if (_isAtBottom) {
_fetchMessages();
}
});
}
void _setupAudioPlayer() {
_playerStateSubscription = _audioPlayer.onPlayerStateChanged.listen((
state,
) {
if (mounted) {
setState(() {
_isPlaying = state == AudioPlayers.PlayerState.playing;
if (state == AudioPlayers.PlayerState.completed ||
state == AudioPlayers.PlayerState.stopped) {
_isPlaying = false;
_isAudioLoading = false;
_audioPosition = Duration.zero;
_currentPlayingUrl = null;
}
});
print('Player state changed: $state');
}
});
_positionSubscription = _audioPlayer.onPositionChanged.listen((position) {
if (mounted) {
setState(() => _audioPosition = position);
}
});
_durationSubscription = _audioPlayer.onDurationChanged.listen((duration) {
if (mounted) {
setState(() => _audioDuration = duration ?? Duration.zero);
}
});
}
void _onScroll() {
if (_scrollController.hasClients) {
final maxScroll = _scrollController.position.maxScrollExtent;
final currentScroll = _scrollController.position.pixels;
_isAtBottom = currentScroll >= maxScroll - 50;
try {
// Store current scroll position
if (_scrollController.hasClients) {
_lastScrollPosition = _scrollController.position.pixels;
}
setState(() {
if (fromMessageId == null) {
isLoading = true;
} else {
isLoadingMore = true;
}
errorMessage = null;
});
print(
'Get messages request: phone_number=${widget.phoneNumber}, chat_id=$
{widget.chatId}, from_message_id=${fromMessageId ?? 0}',
);
print('Get messages response: ${response.statusCode} ${response.body}');
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
final newMessages = (data['messages'] as List<dynamic>)
.map((json) => Message.fromJson(json))
.toList();
if (mounted) {
setState(() {
// Create a map of existing messages for quick lookup
final existingMessages = {for (var msg in messages) msg.id: msg};
if (newMessages.isNotEmpty) {
oldestMessageId = messages.first.id;
}
isLoading = false;
isLoadingMore = false;
if (mounted) {
setState(() {
_isPlaying = true;
_isAudioLoading = false;
_currentPlayingUrl = url;
});
}
} catch (e) {
print('Playback error for $url: $e');
if (mounted) {
setState(() {
_isAudioLoading = false;
_isPlaying = false;
errorMessage = 'خطا در پخش پیام صوتی: $e';
});
}
}
}
@override
void dispose() {
_pollTimer?.cancel();
_scrollController.dispose();
_messageController.dispose();
_audioPlayer.dispose();
_recorder.dispose();
_playerStateSubscription?.cancel();
_positionSubscription?.cancel();
_durationSubscription?.cancel();
_recordingTimer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.chatTitle)),
body: Column(
children: [
Expanded(
child: isLoading
? const Center(child: CircularProgressIndicator())
: messages.isEmpty
? const Center(child: Text('))'هیچ پیامی موجود نیست
: ListView.builder(
controller: _scrollController,
reverse: true,
itemCount: messages.length + (isLoadingMore ? 1 : 0),
itemBuilder: (context, index) {
if (index == messages.length) {
return const Center(child: CircularProgressIndicator());
}
final message = messages[messages.length - 1 - index];
return ListTile(
title: Align(
alignment: message.isOutgoing
? Alignment.centerRight
: Alignment.centerLeft,
child: Container(
padding: const EdgeInsets.all(8.0),
margin: const EdgeInsets.symmetric(
vertical: 4.0,
horizontal: 8.0,
),
decoration: BoxDecoration(
color: message.isOutgoing
? Colors.blue[100]
: Colors.grey[200],
borderRadius: BorderRadius.circular(8.0),
),
child: message.isVoice
? Column(
crossAxisAlignment: message.isOutgoing
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(
_isPlaying &&
_currentPlayingUrl ==
message.voiceUrl
? Icons.pause
: Icons.play_arrow,
),
onPressed:
_isAudioLoading &&
_currentPlayingUrl ==
message.voiceUrl
? null
: () {
if (_isPlaying &&
_currentPlayingUrl ==
message.voiceUrl) {
_stopAudio();
} else {
_playVoice(
message.voiceUrl,
);
}
},
),
Text(
'${message.duration ?? 0} 'ثانیه,
),
],
),
if (_isWaveformLoading &&
message.isOutgoing &&
message.voiceUrl == null)
const SizedBox(
height: 20,
child: LinearProgressIndicator(),
),
if (message.waveformData != null &&
(!_isWaveformLoading ||
message.voiceUrl != null))
SizedBox(
height: 40,
width: 100,
child: CustomPaint(
painter: WaveformPainter(
data: message.waveformData!,
isPlaying:
_isPlaying &&
_currentPlayingUrl ==
message.voiceUrl,
progress:
_audioDuration
.inMilliseconds >
0
? _audioPosition
.inMilliseconds /
_audioDuration
.inMilliseconds
: 0.0,
),
),
),
if (_isPlaying &&
_currentPlayingUrl ==
message.voiceUrl)
Text(
'${_audioPosition.inSeconds} / $
{_audioDuration.inSeconds} 'ثانیه,
style: const TextStyle(fontSize: 10),
),
],
)
: Text(message.content ?? ''),
),
),
subtitle: Text(
message.date.toString().substring(0, 16),
style: const TextStyle(fontSize: 10),
),
);
},
),
),
if (errorMessage != null)
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
errorMessage!,
style: const TextStyle(color: Colors.red),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
child: TextField(
controller: _messageController,
decoration: const InputDecoration(
hintText: 'پیام خود را بنویسید...',
border: OutlineInputBorder(),
),
textDirection: TextDirection.rtl,
),
),
IconButton(
icon: const Icon(Icons.send),
onPressed: _sendMessage,
),
IconButton(
icon: Icon(_isRecording ? Icons.stop : Icons.mic),
onPressed: _isRecording ? _stopRecording : _startRecording,
),
],
),
),
],
),
);
}
}
WaveformPainter({
required this.data,
required this.isPlaying,
required this.progress,
});
@override
void paint(Canvas canvas, Size size) {
final barWidth = 1.0;
final barSpacing = 0.5;
final totalBarWidth = barWidth + barSpacing;
final barCount = (size.width / totalBarWidth).floor();
final height = size.height;
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromLTWH(x, height / 2 - amplitude, barWidth, amplitude * 2),
const Radius.circular(1.0),
),
paint,
);
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromLTWH(x, height / 2 - amplitude, barWidth, amplitude * 2),
const Radius.circular(1.0),
),
strokePaint,
);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
final oldPainter = oldDelegate as WaveformPainter;
return oldPainter.data != data ||
oldPainter.isPlaying != isPlaying ||
oldPainter.progress != progress;
}
}