Sync update: 2026-05-18 16:14:25
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../models/message_model.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
import '../services/whatsapp_service.dart';
|
||||
|
||||
class MessageBubble extends StatelessWidget {
|
||||
final MessageModel message;
|
||||
@@ -58,9 +61,9 @@ class MessageBubble extends StatelessWidget {
|
||||
const SizedBox(height: 4),
|
||||
],
|
||||
|
||||
// Media placeholder if it is media
|
||||
// Interactive Media widget if message has media
|
||||
if (message.hasMedia) ...[
|
||||
_buildMediaPlaceholder(),
|
||||
InteractiveMediaWidget(message: message),
|
||||
const SizedBox(height: 6),
|
||||
],
|
||||
|
||||
@@ -108,49 +111,6 @@ class MessageBubble extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMediaPlaceholder() {
|
||||
IconData iconData = Icons.insert_drive_file;
|
||||
String label = "File Attachment";
|
||||
|
||||
switch (message.type) {
|
||||
case "image":
|
||||
iconData = Icons.photo;
|
||||
label = "Image";
|
||||
break;
|
||||
case "video":
|
||||
iconData = Icons.videocam;
|
||||
label = "Video";
|
||||
break;
|
||||
case "audio":
|
||||
iconData = Icons.audiotrack;
|
||||
label = "Audio File";
|
||||
break;
|
||||
case "sticker":
|
||||
iconData = Icons.emoji_emotions;
|
||||
label = "Sticker";
|
||||
break;
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(iconData, color: AppTheme.textSecondary, size: 32),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(color: AppTheme.textPrimary, fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAckIcon(int ack) {
|
||||
switch (ack) {
|
||||
case 1: // Pending
|
||||
@@ -172,3 +132,184 @@ class MessageBubble extends StatelessWidget {
|
||||
return DateFormat('h:mm a').format(dt);
|
||||
}
|
||||
}
|
||||
|
||||
class InteractiveMediaWidget extends StatefulWidget {
|
||||
final MessageModel message;
|
||||
|
||||
const InteractiveMediaWidget({super.key, required this.message});
|
||||
|
||||
@override
|
||||
State<InteractiveMediaWidget> createState() => _InteractiveMediaWidgetState();
|
||||
}
|
||||
|
||||
class _InteractiveMediaWidgetState extends State<InteractiveMediaWidget> {
|
||||
final WhatsAppService _svc = Get.find<WhatsAppService>();
|
||||
bool _isLoading = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(() {
|
||||
final cachedMedia = _svc.mediaCache[widget.message.id];
|
||||
|
||||
if (cachedMedia != null) {
|
||||
return _buildDownloadedMedia(cachedMedia);
|
||||
}
|
||||
|
||||
if (_isLoading) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
width: 140,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(strokeWidth: 2, color: AppTheme.primary),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Tap to download media placeholder
|
||||
return GestureDetector(
|
||||
onTap: _downloadMedia,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(_getIcon(), color: AppTheme.textSecondary, size: 32),
|
||||
const SizedBox(width: 12),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
_getLabel(),
|
||||
style: const TextStyle(color: AppTheme.textPrimary, fontWeight: FontWeight.w500, fontSize: 13),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
const Text(
|
||||
'Tap to download',
|
||||
style: TextStyle(color: AppTheme.textSecondary, fontSize: 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _downloadMedia() async {
|
||||
setState(() => _isLoading = true);
|
||||
await _svc.downloadAndCacheMedia(widget.message.id);
|
||||
if (mounted) {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildDownloadedMedia(String base64Data) {
|
||||
final bytes = base64Decode(base64Data);
|
||||
|
||||
if (widget.message.type == "image" || widget.message.type == "sticker") {
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.memory(
|
||||
bytes,
|
||||
fit: BoxFit.cover,
|
||||
maxHeight: 250,
|
||||
width: double.infinity,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.message.type == "audio") {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.play_arrow, color: AppTheme.primary, size: 24),
|
||||
onPressed: () {
|
||||
Get.snackbar(
|
||||
'Audio Playback',
|
||||
'Playing voice note/audio file...',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: AppTheme.surfaceLight,
|
||||
colorText: AppTheme.textPrimary,
|
||||
);
|
||||
},
|
||||
),
|
||||
const Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
child: LinearProgressIndicator(
|
||||
value: 0.0,
|
||||
backgroundColor: AppTheme.surfaceLight,
|
||||
color: AppTheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Text(
|
||||
'Voice Note',
|
||||
style: TextStyle(color: AppTheme.textSecondary, fontSize: 11),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Default download complete file placeholder
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.check_circle_outline, color: AppTheme.primary, size: 32),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
_getLabel(),
|
||||
style: const TextStyle(color: AppTheme.textPrimary, fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
IconData _getIcon() {
|
||||
switch (widget.message.type) {
|
||||
case "image": return Icons.photo_outlined;
|
||||
case "video": return Icons.videocam_outlined;
|
||||
case "audio": return Icons.audiotrack_outlined;
|
||||
case "sticker": return Icons.emoji_emotions_outlined;
|
||||
default: return Icons.insert_drive_file_outlined;
|
||||
}
|
||||
}
|
||||
|
||||
String _getLabel() {
|
||||
switch (widget.message.type) {
|
||||
case "image": return "Image Attachment";
|
||||
case "video": return "Video Attachment";
|
||||
case "audio": return "Audio / Voice Note";
|
||||
case "sticker": return "Sticker Attachment";
|
||||
default: return "File Attachment";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user