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

570 lines
19 KiB
Dart

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:file_picker/file_picker.dart';
import 'package:image_picker/image_picker.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:taxglide/consts/download_helper.dart';
import 'package:taxglide/controller/api_contoller.dart';
import 'package:taxglide/controller/api_repository.dart';
import 'package:taxglide/view/Mahi_chat/live_chat_screen.dart';
// ⭐⭐⭐ CHAT INPUT BOX WITH FILE TAG SUPPORT ⭐⭐⭐
class ChatInputBox extends ConsumerStatefulWidget {
final int chatId;
final VoidCallback? onMessageSent;
const ChatInputBox(this.chatId, {Key? key, this.onMessageSent})
: super(key: key);
@override
ConsumerState<ChatInputBox> createState() => _ChatInputBoxState();
}
class _ChatInputBoxState extends ConsumerState<ChatInputBox> {
final TextEditingController messageCtrl = TextEditingController();
final ImagePicker picker = ImagePicker();
List<File> selectedFiles = [];
bool _isSending = false;
@override
void initState() {
super.initState();
_requestPermissions();
}
// ⭐ Request storage permissions
Future<void> _requestPermissions() async {
await DownloadHelper.requestStoragePermission();
}
void showPickerMenu() {
showModalBottomSheet(
context: context,
backgroundColor: Colors.white,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(15)),
),
builder: (context) {
return SafeArea(
bottom: true,
child: SizedBox(
height: 220,
child: Column(
children: [
const SizedBox(height: 10),
ListTile(
leading: const Icon(Icons.photo_camera, size: 28),
title: const Text("Camera", style: TextStyle(fontSize: 17)),
onTap: pickFromCamera,
),
ListTile(
leading: const Icon(Icons.photo_library, size: 28),
title: const Text("Gallery", style: TextStyle(fontSize: 17)),
onTap: pickFromGallery,
),
ListTile(
leading: const Icon(Icons.attach_file, size: 28),
title: const Text("Files", style: TextStyle(fontSize: 17)),
onTap: pickDocuments,
),
const SizedBox(height: 10),
],
),
),
);
},
);
}
Future pickFromCamera() async {
Navigator.pop(context);
try {
var status = await Permission.camera.status;
// Handle cases where we cannot show the system dialog again
// Skip strict blocks on iOS Simulator by just trying to launch picker
if (!Platform.isIOS || status.isGranted || status.isLimited) {
if (status.isPermanentlyDenied || (Platform.isIOS && status.isDenied)) {
status = await Permission.camera.request();
if (!status.isGranted && !status.isLimited) {
_showPermissionDialog("Camera");
return;
}
}
}
final XFile? image = await picker.pickImage(
source: ImageSource.camera,
maxWidth: 1920,
maxHeight: 1080,
imageQuality: 85,
);
if (image != null) {
setState(() => selectedFiles.add(File(image.path)));
}
} catch (e) {
debugPrint("❌ Camera error: $e");
// If it's a simulator, it will throw an error since there is no camera
if (Platform.isIOS && e.toString().contains("camera not available")) {
_showErrorSnackbar("Camera not available on iOS Simulator");
} else {
_showErrorSnackbar("Failed to capture image");
}
}
}
Future pickFromGallery() async {
Navigator.pop(context);
try {
var status = await Permission.photos.status;
if (status.isPermanentlyDenied || (Platform.isIOS && status.isDenied)) {
status = await Permission.photos.request();
if (!status.isGranted && !status.isLimited && !Platform.isIOS) {
_showPermissionDialog("Photos");
return;
}
}
final List<XFile>? images = await picker.pickMultiImage(
maxWidth: 1920,
maxHeight: 1080,
imageQuality: 85,
);
if (images != null && images.isNotEmpty) {
setState(() {
selectedFiles.addAll(images.map((e) => File(e.path)));
});
}
} catch (e) {
print("❌ Gallery error: $e");
_showErrorSnackbar("Failed to pick images");
}
}
Future pickDocuments() async {
Navigator.pop(context);
try {
FilePickerResult? result = await FilePicker.platform.pickFiles(
allowMultiple: true,
type: FileType.any,
);
if (result != null && result.files.isNotEmpty) {
setState(() {
selectedFiles.addAll(
result.paths.where((e) => e != null).map((e) => File(e!)),
);
});
}
} catch (e) {
print("❌ File picker error: $e");
_showErrorSnackbar("Failed to pick files");
}
}
void _showPermissionDialog(String permissionName) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text("$permissionName Permission Required"),
content: Text(
"Allow Taxglide to access your $permissionName to capture and upload files easily. You can enable this in the app settings.",
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("Cancel"),
),
TextButton(
onPressed: () {
Navigator.pop(context);
openAppSettings();
},
child: const Text("Go to Settings"),
),
],
),
);
}
void _showErrorSnackbar(String message) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Colors.red,
content: Text(message),
duration: const Duration(seconds: 2),
),
);
}
void removeFile(int index) {
setState(() {
selectedFiles.removeAt(index);
});
}
String getFileName(String path) {
return path.split('/').last;
}
IconData getFileIcon(String path) {
final ext = path.split('.').last.toLowerCase();
if (['jpg', 'jpeg', 'png', 'gif', 'webp'].contains(ext)) {
return Icons.image;
} else if (ext == 'pdf') {
return Icons.picture_as_pdf;
} else if (['doc', 'docx'].contains(ext)) {
return Icons.description;
} else if (['xls', 'xlsx'].contains(ext)) {
return Icons.table_chart;
} else {
return Icons.insert_drive_file;
}
}
Future<void> sendMessage() async {
if (_isSending) return; // Prevent double-tap
String text = messageCtrl.text.trim();
if (text.isEmpty && selectedFiles.isEmpty) return;
// ⭐ Get the tagged message
final taggedMessage = ref.read(taggedMessageProvider);
final tagId = taggedMessage?.id ?? 0;
setState(() => _isSending = true);
// Show loading dialog
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => WillPopScope(
onWillPop: () async => false,
child: const Center(
child: Card(
child: Padding(
padding: EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text("Sending message..."),
],
),
),
),
),
),
);
try {
List<File> filesToUpload = [];
// ⭐⭐⭐ Save files to Send folder
if (selectedFiles.isNotEmpty) {
print("📤 Saving ${selectedFiles.length} files to Send folder...");
for (File file in selectedFiles) {
try {
// Try to save to Send folder, fallback to original if fails
final savedPath = await DownloadHelper.saveToSendFolder(file);
filesToUpload.add(File(savedPath));
} catch (e) {
print("⚠️ Could not save file, using original: $e");
filesToUpload.add(file);
}
}
print("✅ Files prepared for upload: ${filesToUpload.length}");
}
// Send message with files
await ApiRepository().sendChatMessage(
chatId: widget.chatId,
messages: text,
tagId: tagId,
files: filesToUpload,
);
// Refresh chat messages
await ref.read(chatMessagesProvider(widget.chatId).notifier).refresh();
ref.invalidate(chatDocumentProvider);
print("✅ Message sent successfully with tagId: $tagId");
// Close loading dialog
if (mounted) Navigator.pop(context);
// Clear input
if (mounted) {
setState(() {
messageCtrl.clear();
selectedFiles.clear();
_isSending = false;
});
}
// Clear the tagged message
ref.read(taggedMessageProvider.notifier).state = null;
// Notify parent to scroll to bottom
if (widget.onMessageSent != null) {
widget.onMessageSent!();
}
} catch (e) {
print("❌ Error sending message: $e");
// Close loading dialog
if (mounted) Navigator.pop(context);
setState(() => _isSending = false);
// Show error message
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Colors.red,
content: Row(
children: [
const Icon(Icons.error_outline, color: Colors.white),
const SizedBox(width: 8),
Expanded(
child: Text(
"Failed to send: ${e.toString()}",
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
duration: const Duration(seconds: 4),
action: SnackBarAction(
label: "Retry",
textColor: Colors.white,
onPressed: sendMessage,
),
),
);
}
}
}
@override
Widget build(BuildContext context) {
final taggedMessage = ref.watch(taggedMessageProvider);
return SafeArea(
bottom: true,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
color: Colors.white,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// ⭐⭐⭐ SELECTED FILES PREVIEW (like WhatsApp)
if (selectedFiles.isNotEmpty)
Container(
height: 100,
margin: const EdgeInsets.only(bottom: 10),
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: selectedFiles.length,
itemBuilder: (context, index) {
final file = selectedFiles[index];
final isImage = DownloadHelper.isImageFile(file.path);
return Container(
width: 80,
margin: const EdgeInsets.only(right: 8),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[300]!),
borderRadius: BorderRadius.circular(8),
),
child: Stack(
children: [
// File preview
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: isImage
? Image.file(
file,
width: 80,
height: 100,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return const Center(
child: Icon(Icons.broken_image),
);
},
)
: Center(
child: Column(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Icon(
getFileIcon(file.path),
size: 30,
color: Colors.grey[600],
),
const SizedBox(height: 4),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 4,
),
child: Text(
getFileName(file.path),
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 9),
),
),
],
),
),
),
// Remove button
Positioned(
top: 2,
right: 2,
child: GestureDetector(
onTap: () => removeFile(index),
child: Container(
padding: const EdgeInsets.all(2),
decoration: const BoxDecoration(
color: Colors.black54,
shape: BoxShape.circle,
),
child: const Icon(
Icons.close,
size: 16,
color: Colors.white,
),
),
),
),
],
),
);
},
),
),
// Input Row
Row(
children: [
Expanded(
child: Container(
constraints: const BoxConstraints(
minHeight: 62,
maxHeight: 150,
),
padding: const EdgeInsets.symmetric(horizontal: 10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(7),
border: Border.all(color: const Color(0xFFCACACA)),
boxShadow: const [
BoxShadow(
color: Colors.black26,
offset: Offset(0, 4),
blurRadius: 7.8,
),
],
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: TextField(
controller: messageCtrl,
maxLines: null,
minLines: 1,
keyboardType: TextInputType.multiline,
textInputAction: TextInputAction.newline,
decoration: InputDecoration(
hintText: taggedMessage != null
? "Reply to ${taggedMessage.user?.name ?? 'message'}"
: "Send your message",
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(
vertical: 12,
),
),
),
),
const SizedBox(width: 8),
GestureDetector(
onTap: _isSending ? null : showPickerMenu,
child: Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Icon(
Icons.attach_file,
size: 26,
color: _isSending ? Colors.grey : Colors.black,
),
),
),
],
),
),
),
const SizedBox(width: 10),
GestureDetector(
onTap: _isSending ? null : sendMessage,
child: Container(
height: 46,
width: 46,
decoration: BoxDecoration(
color: _isSending ? Colors.grey : const Color(0xFF08710C),
shape: BoxShape.circle,
),
child: Center(
child: _isSending
? const SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: const Icon(
Icons.send,
color: Colors.white,
size: 22,
),
),
),
),
],
),
],
),
),
);
}
@override
void dispose() {
messageCtrl.dispose();
super.dispose();
}
}