chat support download updated
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -5,6 +5,10 @@
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
|
||||
|
||||
|
||||
<application
|
||||
android:label="kent_logistics_app"
|
||||
|
||||
@@ -1,5 +1,37 @@
|
||||
package com.example.kent_logistics_app
|
||||
|
||||
import android.media.MediaScannerConnection
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
class MainActivity : FlutterActivity()
|
||||
class MainActivity : FlutterActivity() {
|
||||
|
||||
private val CHANNEL = "media_scanner"
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
|
||||
MethodChannel(
|
||||
flutterEngine.dartExecutor.binaryMessenger,
|
||||
CHANNEL
|
||||
).setMethodCallHandler { call, result ->
|
||||
if (call.method == "scanFile") {
|
||||
val path = call.argument<String>("path")
|
||||
|
||||
if (path != null) {
|
||||
MediaScannerConnection.scanFile(
|
||||
applicationContext,
|
||||
arrayOf(path),
|
||||
null,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
result.success(null)
|
||||
} else {
|
||||
result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
class ApiConfig {
|
||||
static const String baseUrl = "http://10.11.236.74:8000/api";
|
||||
static const String baseUrl = "http://10.119.0.74:8000/api";
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ class AppConfig {
|
||||
static const String logoUrlEmulator = "http://10.0.2.2:8000/images/kent_logo2.png";
|
||||
|
||||
// For Physical Device (Replace with your actual PC local IP)
|
||||
static const String logoUrlDevice = "http://10.11.236.74:8000/images/kent_logo2.png";
|
||||
static const String logoUrlDevice = "http://10.119.0.74:8000/images/kent_logo2.png";
|
||||
|
||||
// Which one to use?
|
||||
static const String logoUrl = logoUrlDevice; // CHANGE THIS WHEN TESTING ON REAL DEVICE
|
||||
|
||||
@@ -24,6 +24,7 @@ class ChatScreen extends StatefulWidget {
|
||||
class _ChatScreenState extends State<ChatScreen> {
|
||||
final TextEditingController _messageCtrl = TextEditingController();
|
||||
final ScrollController _scrollCtrl = ScrollController();
|
||||
Map<String, dynamic>? uploadingMessage;
|
||||
|
||||
late ChatService _chatService;
|
||||
final ReverbSocketService _socket = ReverbSocketService();
|
||||
@@ -48,18 +49,54 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
|
||||
_initChat();
|
||||
}
|
||||
String _guessMimeType(String path) {
|
||||
final lower = path.toLowerCase();
|
||||
if (lower.endsWith('.jpg') || lower.endsWith('.png')) return 'image/*';
|
||||
if (lower.endsWith('.mp4')) return 'video/*';
|
||||
if (lower.endsWith('.pdf')) return 'application/pdf';
|
||||
return 'application/octet-stream';
|
||||
}
|
||||
|
||||
Future<void> _pickAndSendFile() async {
|
||||
final XFile? file = await openFile();
|
||||
final XFile? picked = await openFile();
|
||||
if (picked == null || ticketId == null) return;
|
||||
|
||||
if (file == null || ticketId == null) return;
|
||||
final file = File(picked.path);
|
||||
|
||||
final dartFile = File(file.path);
|
||||
// 1️⃣ Show uploading UI
|
||||
setState(() {
|
||||
uploadingMessage = {
|
||||
'local_file': file,
|
||||
'file_type': _guessMimeType(file.path),
|
||||
'progress': 0.0,
|
||||
};
|
||||
});
|
||||
|
||||
await _chatService.sendFile(ticketId!, dartFile);
|
||||
// 2️⃣ Upload (NO adding message)
|
||||
await _chatService.sendFile(
|
||||
ticketId!,
|
||||
file,
|
||||
onProgress: (progress) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
uploadingMessage!['progress'] = progress;
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// 3️⃣ Remove sending bubble ONLY
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
uploadingMessage = null;
|
||||
});
|
||||
|
||||
// 🚫 DO NOT add message here
|
||||
// WebSocket will handle it
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// ============================
|
||||
// INIT CHAT
|
||||
// ============================
|
||||
@@ -77,10 +114,23 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
context: context,
|
||||
ticketId: ticketId!,
|
||||
onMessage: (msg) {
|
||||
if (!mounted) return;
|
||||
setState(() => messages.add(msg));
|
||||
final incomingClientId = msg['client_id'];
|
||||
|
||||
setState(() {
|
||||
// 🧹 Remove local temp message with same client_id
|
||||
messages.removeWhere(
|
||||
(m) => m['client_id'] != null &&
|
||||
m['client_id'] == incomingClientId,
|
||||
);
|
||||
|
||||
// ✅ Add confirmed socket message
|
||||
messages.add(msg);
|
||||
});
|
||||
|
||||
_scrollToBottom();
|
||||
},
|
||||
|
||||
|
||||
onAdminMessage: () {
|
||||
if (!mounted) {
|
||||
context.read<ChatUnreadProvider>().increment();
|
||||
@@ -112,16 +162,35 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
// ============================
|
||||
Future<void> _sendMessage() async {
|
||||
final text = _messageCtrl.text.trim();
|
||||
if (text.isEmpty) return;
|
||||
if (text.isEmpty || ticketId == null) return;
|
||||
|
||||
_messageCtrl.clear();
|
||||
|
||||
final clientId = DateTime.now().millisecondsSinceEpoch.toString();
|
||||
|
||||
// 1️⃣ ADD LOCAL MESSAGE IMMEDIATELY
|
||||
setState(() {
|
||||
messages.add({
|
||||
'client_id': clientId,
|
||||
'sender_type': 'App\\Models\\User',
|
||||
'message': text,
|
||||
'file_path': null,
|
||||
'file_type': null,
|
||||
'sending': true,
|
||||
});
|
||||
});
|
||||
|
||||
_scrollToBottom();
|
||||
|
||||
// 2️⃣ SEND TO SERVER
|
||||
await _chatService.sendMessage(
|
||||
ticketId!,
|
||||
message: text,
|
||||
clientId: clientId,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// ============================
|
||||
// DISPOSE
|
||||
// ============================
|
||||
@@ -226,47 +295,83 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
|
||||
|
||||
Widget _buildMessages() {
|
||||
return ListView.builder(
|
||||
return ListView(
|
||||
controller: _scrollCtrl,
|
||||
padding: const EdgeInsets.all(12),
|
||||
itemCount: messages.length,
|
||||
itemBuilder: (_, index) {
|
||||
final msg = messages[index];
|
||||
final isUser = msg['sender_type'] == 'App\\Models\\User';
|
||||
children: [
|
||||
// EXISTING MESSAGES
|
||||
...messages.map((msg) {
|
||||
final isUser = msg['sender_type'] == 'App\\Models\\User';
|
||||
|
||||
final String? filePath = msg['file_path'];
|
||||
final String? fileType = msg['file_type'];
|
||||
final String? message = msg['message'];
|
||||
|
||||
return Align(
|
||||
alignment: isUser ? Alignment.centerRight : Alignment.centerLeft,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 6),
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: isUser ? Colors.blue : Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
|
||||
// 🔽 ONLY THIS PART CHANGED
|
||||
child: filePath == null
|
||||
? Text(
|
||||
message ?? '',
|
||||
style: TextStyle(
|
||||
color: isUser ? Colors.white : Colors.black,
|
||||
return Align(
|
||||
alignment: isUser ? Alignment.centerRight : Alignment.centerLeft,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 6),
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: isUser ? Colors.blue : Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: msg['file_path'] == null
|
||||
? Text(
|
||||
msg['message'] ?? '',
|
||||
style: TextStyle(
|
||||
color: isUser ? Colors.white : Colors.black,
|
||||
),
|
||||
)
|
||||
: ChatFilePreview(
|
||||
filePath: msg['file_path'],
|
||||
fileType: msg['file_type'] ?? '',
|
||||
isUser: isUser,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
||||
// ⏳ UPLOADING MESSAGE
|
||||
if (uploadingMessage != null)
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 6),
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ChatFilePreview(
|
||||
filePath: uploadingMessage!['local_file'].path,
|
||||
fileType: uploadingMessage!['file_type'],
|
||||
isUser: true,
|
||||
isLocal: true,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
LinearProgressIndicator(
|
||||
value: uploadingMessage!['progress'],
|
||||
backgroundColor: Colors.white24,
|
||||
valueColor:
|
||||
const AlwaysStoppedAnimation(Colors.white),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const Text(
|
||||
"Sending…",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: ChatFilePreview(
|
||||
filePath: filePath,
|
||||
fileType: fileType ?? '',
|
||||
isUser: isUser,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Widget _buildInput() {
|
||||
return SafeArea(
|
||||
child: Row(
|
||||
|
||||
@@ -24,27 +24,25 @@ class ChatService {
|
||||
Future<void> sendMessage(
|
||||
int ticketId, {
|
||||
String? message,
|
||||
String? filePath,
|
||||
String? clientId,
|
||||
}) async {
|
||||
final form = FormData();
|
||||
|
||||
if (message != null) form.fields.add(MapEntry('message', message));
|
||||
if (filePath != null) {
|
||||
form.files.add(
|
||||
MapEntry(
|
||||
'file',
|
||||
await MultipartFile.fromFile(filePath),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (clientId != null) form.fields.add(MapEntry('client_id', clientId));
|
||||
|
||||
await dio.post('/user/chat/send/$ticketId', data: form);
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------
|
||||
// SEND FILE (image/video/pdf/excel)
|
||||
// ---------------------------
|
||||
Future<void> sendFile(int ticketId, File file) async {
|
||||
Future<Map<String, dynamic>> sendFile(
|
||||
int ticketId,
|
||||
File file, {
|
||||
required Function(double) onProgress,
|
||||
}) async {
|
||||
final formData = FormData.fromMap({
|
||||
'file': await MultipartFile.fromFile(
|
||||
file.path,
|
||||
@@ -52,13 +50,20 @@ class ChatService {
|
||||
),
|
||||
});
|
||||
|
||||
await dio.post(
|
||||
final res = await dio.post(
|
||||
"/user/chat/send/$ticketId",
|
||||
|
||||
data: formData,
|
||||
options: Options(
|
||||
headers: {'Content-Type': 'multipart/form-data'},
|
||||
),
|
||||
onSendProgress: (sent, total) {
|
||||
if (total > 0) {
|
||||
onProgress(sent / total);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return Map<String, dynamic>.from(res.data['message']);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import 'token_interceptor.dart';
|
||||
class DioClient {
|
||||
static Dio? _dio;
|
||||
|
||||
static const String baseUrl = "http://10.11.236.74:8000";
|
||||
static const String baseUrl = "http://10.119.0.74:8000";
|
||||
|
||||
static Dio getInstance(BuildContext context) {
|
||||
if (_dio == null) {
|
||||
|
||||
@@ -36,7 +36,7 @@ class ReverbSocketService {
|
||||
_onAdminMessage = onAdminMessage; // 👈 SAVE
|
||||
|
||||
final uri = Uri.parse(
|
||||
'ws://10.11.236.74:8080/app/q5fkk5rvcnatvbgadwvl'
|
||||
'ws://10.119.0.74:8080/app/q5fkk5rvcnatvbgadwvl'
|
||||
'?protocol=7&client=flutter&version=1.0',
|
||||
);
|
||||
|
||||
@@ -60,6 +60,7 @@ class ReverbSocketService {
|
||||
Future<void> _handleMessage(dynamic raw) async {
|
||||
debugPrint("📥 RAW: $raw");
|
||||
|
||||
|
||||
final payload = jsonDecode(raw);
|
||||
final event = payload['event']?.toString() ?? '';
|
||||
|
||||
@@ -91,13 +92,15 @@ class ReverbSocketService {
|
||||
event == 'NewChatMessage' ||
|
||||
event.endsWith('.NewChatMessage') ||
|
||||
event.contains('NewChatMessage')) {
|
||||
|
||||
dynamic data = payload['data'];
|
||||
if (data is String) data = jsonDecode(data);
|
||||
|
||||
final int msgId = data['id'];
|
||||
final String senderType = data['sender_type'] ?? '';
|
||||
final String? incomingClientId = data['client_id']; // ✅ HERE
|
||||
|
||||
// 🔁 Prevent duplicates
|
||||
// 🔁 Prevent duplicates by DB id
|
||||
if (_receivedIds.contains(msgId)) {
|
||||
debugPrint("🔁 DUPLICATE MESSAGE IGNORED: $msgId");
|
||||
return;
|
||||
@@ -106,20 +109,19 @@ class ReverbSocketService {
|
||||
|
||||
debugPrint("📩 NEW MESSAGE");
|
||||
debugPrint("🆔 id=$msgId");
|
||||
debugPrint("🧩 client_id=$incomingClientId");
|
||||
debugPrint("👤 sender=$senderType");
|
||||
debugPrint("💬 text=${data['message']}");
|
||||
|
||||
// Always push message to UI
|
||||
// ✅ Forward FULL payload (with client_id) to UI
|
||||
_onMessage(Map<String, dynamic>.from(data));
|
||||
|
||||
// 🔔 Increment unread ONLY if ADMIN sent message
|
||||
// 🔔 Increment unread ONLY if ADMIN sent message
|
||||
// 🔔 Unread count only for admin messages
|
||||
if (senderType == 'App\\Models\\Admin') {
|
||||
debugPrint("🔔 ADMIN MESSAGE → UNREAD +1");
|
||||
_onAdminMessage(); // ✅ ACTUAL INCREMENT
|
||||
_onAdminMessage();
|
||||
}
|
||||
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
118
lib/widgets/chat_file_actions.dart
Normal file
118
lib/widgets/chat_file_actions.dart
Normal file
@@ -0,0 +1,118 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import '../screens/chat_file_viewer.dart';
|
||||
|
||||
class ChatFileActions {
|
||||
static void show(
|
||||
BuildContext context, {
|
||||
required String url,
|
||||
required String fileType,
|
||||
}) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
builder: (_) {
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_actionTile(
|
||||
icon: Icons.open_in_new,
|
||||
title: "Open",
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
ChatFileViewer.open(
|
||||
context,
|
||||
url: url,
|
||||
fileType: fileType,
|
||||
);
|
||||
},
|
||||
),
|
||||
_actionTile(
|
||||
icon: Icons.download,
|
||||
title: "Download",
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
await _downloadFile(context, url, fileType);
|
||||
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static Widget _actionTile({
|
||||
required IconData icon,
|
||||
required String title,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return ListTile(
|
||||
leading: Icon(icon),
|
||||
title: Text(title),
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
static Future<void> _scanFile(String path) async {
|
||||
const platform = MethodChannel('media_scanner');
|
||||
|
||||
try {
|
||||
await platform.invokeMethod('scanFile', {'path': path});
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
|
||||
// ===========================
|
||||
// DOWNLOAD LOGIC
|
||||
// ===========================
|
||||
static Future<void> _downloadFile(
|
||||
BuildContext context,
|
||||
String url,
|
||||
String fileType,
|
||||
) async {
|
||||
try {
|
||||
final fileName = url.split('/').last;
|
||||
|
||||
Directory baseDir;
|
||||
|
||||
if (fileType.startsWith('image/')) {
|
||||
baseDir = Directory('/storage/emulated/0/Pictures/KentChat');
|
||||
} else if (fileType.startsWith('video/')) {
|
||||
baseDir = Directory('/storage/emulated/0/Movies/KentChat');
|
||||
} else {
|
||||
baseDir = Directory('/storage/emulated/0/Download/KentChat');
|
||||
}
|
||||
|
||||
if (!await baseDir.exists()) {
|
||||
await baseDir.create(recursive: true);
|
||||
}
|
||||
|
||||
final filePath = "${baseDir.path}/$fileName";
|
||||
|
||||
await Dio().download(url, filePath);
|
||||
|
||||
// 🔔 IMPORTANT: Tell Android to scan the file
|
||||
await _scanFile(filePath);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Saved to ${baseDir.path}")),
|
||||
);
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text("Download failed")),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,99 +1,118 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../services/dio_client.dart';
|
||||
import '../screens/chat_file_viewer.dart';
|
||||
import '../widgets/chat_file_actions.dart';
|
||||
|
||||
class ChatFilePreview extends StatelessWidget {
|
||||
final String filePath;
|
||||
final String fileType;
|
||||
final bool isUser;
|
||||
|
||||
final bool isLocal;
|
||||
|
||||
const ChatFilePreview({
|
||||
super.key,
|
||||
required this.filePath,
|
||||
required this.fileType,
|
||||
required this.isUser,
|
||||
this.isLocal = false,
|
||||
});
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final url = "${DioClient.baseUrl}/storage/$filePath";
|
||||
final textColor = isUser ? Colors.white : Colors.black;
|
||||
|
||||
// IMAGE PREVIEW
|
||||
if (fileType.startsWith('image/')) {
|
||||
return GestureDetector(
|
||||
onTap: () => ChatFileViewer.open(
|
||||
context,
|
||||
url: url,
|
||||
fileType: fileType,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.network(
|
||||
url,
|
||||
width: 180,
|
||||
height: 120,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
// LOCAL IMAGE (uploading preview)
|
||||
if (isLocal && fileType.startsWith('image/')) {
|
||||
return Image.file(
|
||||
File(filePath),
|
||||
width: 180,
|
||||
height: 120,
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
}
|
||||
|
||||
// VIDEO PREVIEW
|
||||
if (fileType.startsWith('video/')) {
|
||||
return GestureDetector(
|
||||
onTap: () => ChatFileViewer.open(
|
||||
context,
|
||||
url: url,
|
||||
fileType: fileType,
|
||||
),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 180,
|
||||
height: 120,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black26,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.videocam,
|
||||
size: 50,
|
||||
color: Colors.white70,
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
Icons.play_circle_fill,
|
||||
size: 56,
|
||||
color: Colors.white,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// PDF / FILE PREVIEW
|
||||
return GestureDetector(
|
||||
onTap: () => ChatFileViewer.open(
|
||||
context,
|
||||
url: url,
|
||||
fileType: fileType,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(_fileIcon(fileType), color: textColor),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
_fileLabel(fileType),
|
||||
style: TextStyle(color: textColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
// ✅ TAP = OPEN
|
||||
ChatFileViewer.open(
|
||||
context,
|
||||
url: url,
|
||||
fileType: fileType,
|
||||
);
|
||||
},
|
||||
onLongPress: () {
|
||||
// ✅ LONG PRESS = OPEN / DOWNLOAD
|
||||
ChatFileActions.show(
|
||||
context,
|
||||
url: url,
|
||||
fileType: fileType,
|
||||
);
|
||||
},
|
||||
child: _buildPreviewUI(textColor, url),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPreviewUI(Color textColor, String url) {
|
||||
// IMAGE
|
||||
if (fileType.startsWith('image/')) {
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.network(
|
||||
url,
|
||||
width: 180,
|
||||
height: 120,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// VIDEO
|
||||
if (fileType.startsWith('video/')) {
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 180,
|
||||
height: 120,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black26,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.videocam,
|
||||
size: 50,
|
||||
color: Colors.white70,
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
Icons.play_circle_fill,
|
||||
size: 56,
|
||||
color: Colors.white,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// PDF / OTHER FILES
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(_fileIcon(fileType), color: textColor),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
_fileLabel(fileType),
|
||||
style: TextStyle(color: textColor),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
IconData _fileIcon(String type) {
|
||||
if (type == 'application/pdf') return Icons.picture_as_pdf;
|
||||
if (type.contains('excel')) return Icons.table_chart;
|
||||
|
||||
Reference in New Issue
Block a user