taxgilde/lib/view/Mahi_chat/file_images_screen.dart
2026-04-11 10:21:31 +05:30

691 lines
30 KiB
Dart

// ⭐⭐⭐ UPDATED SWIPEABLE MESSAGE BUBBLE WITH IMAGE GRID VIEWER ⭐⭐⭐
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:taxglide/consts/download_helper.dart';
import 'package:taxglide/model/chat_model.dart';
class SwipeableMessageBubble extends StatefulWidget {
final MessageModel message;
final DateTime msgDate;
final String Function(DateTime) formatTime;
final Widget Function(int, int) buildTick;
final VoidCallback onSwipe;
final Function(int?)? onParentTagTap;
final Function(List<String> imageUrls, int initialIndex)?
onImageTap; // 🖼️ NEW
const SwipeableMessageBubble({
super.key,
required this.message,
required this.msgDate,
required this.formatTime,
required this.buildTick,
required this.onSwipe,
this.onParentTagTap,
this.onImageTap, // 🖼️ NEW
});
@override
State<SwipeableMessageBubble> createState() => SwipeableMessageBubbleState();
}
class SwipeableMessageBubbleState extends State<SwipeableMessageBubble>
with SingleTickerProviderStateMixin {
double _dragExtent = 0;
late AnimationController _controller;
late Animation<double> _animation;
final Map<String, bool> _downloadingFiles = {};
final Map<String, bool> _downloadedFiles = {};
bool _isHighlighted = false;
void highlight() {
if (!mounted) return;
// Reset first to ensure it re-triggers if already highlighted
setState(() => _isHighlighted = false);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
setState(() => _isHighlighted = true);
Future.delayed(const Duration(milliseconds: 1500), () {
if (mounted) setState(() => _isHighlighted = false);
});
});
}
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 200),
);
_animation = Tween<double>(begin: 0, end: 0).animate(_controller)
..addListener(() {
setState(() {
_dragExtent = _animation.value;
});
});
_checkDownloadedFiles();
}
Future<void> _checkDownloadedFiles() async {
for (var doc in widget.message.uploadedDocuments) {
final existingPath = await DownloadHelper.checkFileExistsInReceived(
doc.filePath,
);
if (existingPath != null && mounted) {
setState(() {
_downloadedFiles[doc.fileName] = true;
});
}
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _handleDragUpdate(DragUpdateDetails details) {
if (!mounted) return;
final isUserMessage = widget.message.chatBy == "user";
final delta = details.primaryDelta ?? 0;
if ((isUserMessage && delta < 0) || (!isUserMessage && delta > 0)) {
setState(() {
_dragExtent += delta;
_dragExtent = _dragExtent.clamp(
isUserMessage ? -80.0 : 0.0,
isUserMessage ? 0.0 : 80.0,
);
});
}
}
void _handleDragEnd(DragEndDetails details) {
if (!mounted) return;
final threshold = 60.0;
if (_dragExtent.abs() > threshold) {
widget.onSwipe();
}
_animation = Tween<double>(
begin: _dragExtent,
end: 0,
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut));
_controller.reset();
_controller.forward();
}
// 🖼️ NEW: Handle image tap - open grid viewer
void _handleImageTap(int tappedIndex) {
if (widget.onImageTap == null) return;
// Get all image URLs from uploaded documents
final imageUrls = widget.message.uploadedDocuments
.where((doc) => DownloadHelper.isImageFile(doc.fileName))
.map((doc) => doc.filePath)
.toList();
if (imageUrls.isNotEmpty) {
widget.onImageTap!(imageUrls, tappedIndex);
}
}
Future<void> _handleFileView(String url, String fileName) async {
if (!mounted) return;
try {
print("👁️ Opening file: $fileName");
final existingPath = await DownloadHelper.checkFileExistsInReceived(url);
if (existingPath != null) {
await DownloadHelper.openDownloadedFile(existingPath);
} else {
final sendFolder = await DownloadHelper.getSendFolder();
final localPath = '${sendFolder.path}/$fileName';
final file = File(localPath);
if (await file.exists()) {
await DownloadHelper.openDownloadedFile(localPath);
} else {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("File not found locally"),
backgroundColor: Colors.orange,
duration: Duration(seconds: 2),
),
);
}
}
}
} catch (e) {
print("❌ View error: $e");
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Could not open file: $e"),
backgroundColor: Colors.red,
),
);
}
}
}
Future<void> _handleFileDownload(String url, String fileName) async {
if (!mounted) return;
setState(() {
_downloadingFiles[fileName] = true;
});
try {
print("🔗 Downloading from: $url");
final result = await DownloadHelper.downloadFileToReceived(url);
if (!mounted) return;
if (result['success'] == true) {
setState(() {
_downloadedFiles[fileName] = true;
});
if (mounted) {
if (result['isNewDownload'] == false) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Opening $fileName"),
backgroundColor: Colors.green,
duration: const Duration(seconds: 1),
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Downloaded $fileName"),
backgroundColor: Colors.green,
duration: const Duration(seconds: 1),
),
);
}
}
}
} catch (e) {
print("❌ Download error: $e");
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Download failed: $e"),
backgroundColor: Colors.red,
),
);
}
} finally {
if (mounted) {
setState(() {
_downloadingFiles[fileName] = false;
});
}
}
}
Future<void> _handleDownloadedFileView(String url, String fileName) async {
if (!mounted) return;
try {
final existingPath = await DownloadHelper.checkFileExistsInReceived(url);
if (existingPath != null) {
await DownloadHelper.openDownloadedFile(existingPath);
}
} catch (e) {
print("❌ View error: $e");
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Could not open file: $e"),
backgroundColor: Colors.red,
),
);
}
}
}
@override
Widget build(BuildContext context) {
final isUserMessage = widget.message.chatBy == "user";
final hasDocuments = widget.message.uploadedDocuments.isNotEmpty;
print("💬 Building message bubble:");
print(" - ID: ${widget.message.id}");
print(" - chatBy: ${widget.message.chatBy}");
print(
" - uploadedDocuments count: ${widget.message.uploadedDocuments.length}",
);
return Stack(
children: [
if (_dragExtent.abs() > 10)
Positioned(
right: isUserMessage ? 20 : null,
left: isUserMessage ? null : 20,
top: 0,
bottom: 0,
child: Center(
child: Icon(
Icons.reply_rounded,
color: Colors.grey[600],
size: 24,
),
),
),
GestureDetector(
onHorizontalDragUpdate: _handleDragUpdate,
onHorizontalDragEnd: _handleDragEnd,
child: Transform.translate(
offset: Offset(_dragExtent, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (widget.message.user != null)
Padding(
padding: const EdgeInsets.only(left: 5, right: 5),
child: Align(
alignment: isUserMessage
? Alignment.centerRight
: Alignment.centerLeft,
child: Text(
widget.message.user!.name,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
),
),
if (widget.message.parentTag != null)
GestureDetector(
onTap: () {
if (widget.onParentTagTap != null) {
widget.onParentTagTap!(
widget.message.parentTag?.id ?? widget.message.tagId,
);
}
},
child: Align(
alignment: isUserMessage
? Alignment.centerRight
: Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.only(
bottom: 4,
left: 5,
right: 5,
),
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 6,
),
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.60,
),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.05),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: const Color(0xFF630A73).withOpacity(0.3),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 3,
height: 30,
decoration: BoxDecoration(
color: const Color(0xFF630A73),
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 8),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.message.parentTag!.type == "file")
Row(
children: [
Icon(
Icons.insert_drive_file,
size: 14,
color: Colors.grey[600],
),
const SizedBox(width: 4),
Expanded(
child: Text(
widget
.message
.parentTag!
.fileName ??
"File",
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: Colors.grey[700],
),
),
),
],
),
Text(
widget.message.parentTag!.message,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12,
color: Colors.grey[700],
),
),
],
),
),
],
),
),
),
),
Align(
alignment: isUserMessage
? Alignment.centerRight
: Alignment.centerLeft,
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.70,
),
child: Container(
margin: const EdgeInsets.symmetric(vertical: 6),
padding: const EdgeInsets.symmetric(
vertical: 10,
horizontal: 14,
),
decoration: BoxDecoration(
color: _isHighlighted
? Colors.green.withOpacity(0.2)
: (isUserMessage
? const Color(0xFFFFF8E2)
: Colors.white),
border: Border.all(
color: _isHighlighted
? Colors.green.withOpacity(0.5)
: (isUserMessage
? const Color(0xFFF9D9C8)
: const Color(0xFFE2E2E2)),
),
borderRadius: BorderRadius.circular(10),
boxShadow: isUserMessage
? []
: const [
BoxShadow(
color: Color(0x40979797),
blurRadius: 3.6,
offset: Offset(0, 1),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ⭐⭐⭐ DISPLAY uploadedDocuments WITH IMAGE GRID SUPPORT
if (hasDocuments)
...widget.message.uploadedDocuments.asMap().entries.map((
entry,
) {
final index = entry.key;
final doc = entry.value;
final isImage = DownloadHelper.isImageFile(
doc.fileName,
);
final isDownloading =
_downloadingFiles[doc.fileName] ?? false;
final isDownloaded =
_downloadedFiles[doc.fileName] ?? false;
// 🖼️ Calculate image index (only count images before this one)
final imageIndex = widget
.message
.uploadedDocuments
.sublist(0, index)
.where(
(d) =>
DownloadHelper.isImageFile(d.fileName),
)
.length;
return GestureDetector(
onTap: isImage
? () =>
_handleImageTap(
imageIndex,
) // 🖼️ Open grid viewer
: (isUserMessage
? () => _handleFileView(
doc.filePath,
doc.fileName,
)
: null),
child: Container(
margin: const EdgeInsets.only(bottom: 8),
child: isImage
? Stack(
children: [
ClipRRect(
borderRadius:
BorderRadius.circular(8),
child: Image.network(
doc.filePath,
width: 150,
height: 100,
fit: BoxFit.cover,
errorBuilder:
(context, error, stack) {
return Container(
width: 200,
height: 150,
color: Colors.grey[300],
child: const Center(
child: Icon(
Icons.broken_image,
),
),
);
},
),
),
// ⭐ Download/View icon for receiver messages
if (!isUserMessage)
Positioned(
top: 8,
right: 8,
child: GestureDetector(
onTap: isDownloading
? null
: isDownloaded
? () =>
_handleDownloadedFileView(
doc.filePath,
doc.fileName,
)
: () =>
_handleFileDownload(
doc.filePath,
doc.fileName,
),
child: Container(
padding:
const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.black
.withOpacity(0.6),
shape: BoxShape.circle,
),
child: isDownloading
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor:
AlwaysStoppedAnimation<
Color
>(
Colors
.white,
),
),
)
: Icon(
isDownloaded
? Icons
.visibility
: Icons
.download,
color: Colors.white,
size: 20,
),
),
),
),
],
)
: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(
8,
),
border: Border.all(
color: Colors.grey[300]!,
),
),
child: Row(
children: [
Icon(
Icons.insert_drive_file,
color: Colors.grey[600],
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
doc.fileName,
style: const TextStyle(
fontSize: 13,
fontWeight:
FontWeight.w600,
),
),
Text(
doc.fileType
.toUpperCase(),
style: TextStyle(
fontSize: 11,
color: Colors.grey[600],
),
),
],
),
),
GestureDetector(
onTap: isDownloading
? null
: () => isUserMessage
? _handleFileView(
doc.filePath,
doc.fileName,
)
: isDownloaded
? _handleDownloadedFileView(
doc.filePath,
doc.fileName,
)
: _handleFileDownload(
doc.filePath,
doc.fileName,
),
child: isDownloading
? const SizedBox(
width: 20,
height: 20,
child:
CircularProgressIndicator(
strokeWidth: 2,
),
)
: Icon(
isUserMessage
? Icons.open_in_new
: isDownloaded
? Icons.visibility
: Icons.download,
color: Colors.grey[600],
size: 20,
),
),
],
),
),
),
);
}).toList(),
if (widget.message.message.isNotEmpty)
Text(
widget.message.message,
style: const TextStyle(fontSize: 15),
),
const SizedBox(height: 3),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
widget.formatTime(widget.msgDate),
style: TextStyle(
fontSize: 11,
color: Colors.grey[700],
),
),
const SizedBox(width: 4),
widget.buildTick(
widget.message.isDelivered,
widget.message.isRead,
),
],
),
],
),
),
),
),
],
),
),
),
],
);
}
}