import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:http/http.dart' as http; import 'shared_preferences_provider.dart'; import 'custom_menu.dart'; class StatusPage extends StatefulWidget { final VoidCallback toggleProfile; const StatusPage({super.key, required this.toggleProfile}); @override StatusPageState createState() => StatusPageState(); } class StatusPageState extends State with WidgetsBindingObserver { late final WebSocketChannel channel; List> messages = []; final Map _imageCache = {}; static const wsBaseUrl = String.fromEnvironment('WS_BASE_URL', defaultValue: 'ws://localhost:8080'); static const restBaseUrl = String.fromEnvironment('REST_BASE_URL', defaultValue: 'http://localhost:8080'); @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); _initializeWebSocket(); } void _initializeWebSocket() { channel = WebSocketChannel.connect( Uri.parse('$wsBaseUrl/ws'), ); debugPrint("WebSocket initialized at: $wsBaseUrl/ws"); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); channel.sink.close(); debugPrint("WebSocket connection closed."); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { debugPrint("App resumed"); updateStatus(); updateMessages(); } } void updateStatus() async { final url = Uri.parse('$restBaseUrl/get'); final prefsProvider = Provider.of(context, listen: false); try { debugPrint("Updating status..."); final response = await http.post( url, headers: {'Content-Type': 'application/json'}, body: jsonEncode({"Id: $prefsProvider.getUserId()"}), ); debugPrint(response.statusCode.toString()); if (response.statusCode == 200) { final jsonResponse = jsonDecode(response.body) as Map; // Extract the 'Status' field or other relevant fields from the response String newStatus = jsonResponse['Status'] ?? 'none'; debugPrint("Starting status update: $newStatus"); // Store the new status in SharedPreferences await prefsProvider.setCurrentStatus(newStatus); debugPrint("Status updated: $newStatus"); } else { debugPrint("Failed to update status: ${response.statusCode}"); } } catch (e) { debugPrint("Error in updateMessages: $e"); } } void updateMessages() async { final url = Uri.parse('$restBaseUrl/update'); try { final response = await http.get( url, headers: {'Content-Type': 'application/json'}, ); debugPrint("Response body: ${response.body}\n"); if (response.statusCode == 200) { final parsed = jsonDecode(response.body); // Ensure 'parsed' is a List, otherwise handle gracefully if (parsed is List) { debugPrint("Messages to update: ${parsed.length}"); // Ensure each item is a Map final List> jsonResponse = parsed .where((item) => item is Map) .map((item) => item as Map) .toList(); setState(() { messages.clear(); // Clear the existing messages messages.addAll(jsonResponse); // Add new messages }); } else { debugPrint("API returned unexpected format: ${response.body}"); setState(() { messages.clear(); // Clear messages if format is invalid }); } } else { debugPrint("Failed to fetch messages: ${response.statusCode}"); } } catch (e) { debugPrint("Error in updateMessages: $e"); } } List> _getMessagesByStatus(String status) { return messages.where((message) => message['Status'] == status).toList(); } void _sendStatus(String id, String name, String? image, String status) async { debugPrint("Entering _sendStatus"); final prefsProvider = Provider.of(context, listen: false); debugPrint("Is mounted: $mounted"); if (!mounted) return; debugPrint("Sending status: $status"); final isStatusActive = prefsProvider.getCurrentStatus() == status; debugPrint("Is status active: $isStatusActive"); final newStatus = isStatusActive ? 'none' : status; debugPrint("New status: $newStatus"); // Update local status in SharedPreferences await prefsProvider.setCurrentStatus(newStatus); debugPrint(prefsProvider.getCurrentStatus()); // Create the status message final message = { 'Id': id, 'Name': name, 'Image': image, 'Status': newStatus.isEmpty ? 'none' : newStatus, 'Timestamp': DateTime.now().toIso8601String(), }; // Update the local messages list with the new status message setState(() { // Remove existing message from the same user to avoid duplicates messages.removeWhere((msg) => msg['Id'] == id); // Add the updated message to the local messages list if (newStatus != 'none') { messages.add(message); debugPrint("Adding local message: $newStatus"); } }); // Send the updated status message to the WebSocket for other users //channel?.sink.add(jsonEncode(message)); // Send the status message to the REST API as well final url = Uri.parse('$restBaseUrl/set'); try { final response = await http.post( url, headers: {'Content-Type': 'application/json'}, body: jsonEncode(message), ); if (!mounted) return; // Check if widget is still in the tree if (response.statusCode == 200) { Fluttertoast.showToast( msg: 'Status "${newStatus == 'none' ? 'cleared' : newStatus}" sent!', toastLength: Toast.LENGTH_SHORT, gravity: ToastGravity.BOTTOM, backgroundColor: Colors.blueAccent, webBgColor: "#0000FF", textColor: Colors.white, fontSize: 16.0, timeInSecForIosWeb: 1, ); } else { Fluttertoast.showToast( msg: 'Failed to send status. Please try again.', toastLength: Toast.LENGTH_SHORT, gravity: ToastGravity.BOTTOM, backgroundColor: Colors.redAccent, webBgColor: "#FF0000", textColor: Colors.white, fontSize: 16.0, timeInSecForIosWeb: 1, ); } } catch (e) { if (!mounted) return; Fluttertoast.showToast( msg: 'Error sending status. Please check your connection.', toastLength: Toast.LENGTH_SHORT, gravity: ToastGravity.BOTTOM, backgroundColor: Colors.redAccent, webBgColor: "#FF0000", textColor: Colors.white, fontSize: 16.0, timeInSecForIosWeb: 1, ); debugPrint("Error in _sendStatus: $e"); } } void _handleIncomingMessage(Map message) async { final prefsProvider = Provider.of(context, listen: false); final status = message['Status']; final incomingId = message['Id']; final image = message['Image']; final timeStamp = message['Timestamp']; final prefsId = prefsProvider.getUserId(); debugPrint( "Incoming id $incomingId, status: $status, prefsID: $prefsId, timestamp: $timeStamp"); if (incomingId == prefsProvider.getUserId()) { if (status == 'removed' || status == 'none') { debugPrint("Clearing local message: $status"); await prefsProvider.setCurrentStatus('none'); } else { debugPrint("Ignoring own message: $status"); return; } } else { if (status == 'removed' || status == 'none') { debugPrint("Checking for messages from user: $incomingId"); if (messages.any((msg) => msg['Id'] == incomingId)) { debugPrint("Removing message from user: $incomingId"); messages.removeWhere((msg) => msg['Id'] == incomingId); } } else { debugPrint("Adding incoming message: $status"); messages.add(message); _cacheImage(incomingId, image); } } } void _cacheImage(String id, String? base64Image) { if (base64Image != null) { _imageCache[id] = Image.memory(base64Decode(base64Image)).image; } } void _showImageDialog(BuildContext context, ImageProvider imageProvider) { showDialog( context: context, builder: (BuildContext context) { return Dialog( backgroundColor: Colors.transparent, child: GestureDetector( onTap: () => Navigator.of(context).pop(), child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), color: Colors.black87, ), padding: const EdgeInsets.all(8.0), child: Image(image: imageProvider, fit: BoxFit.contain), ), ), ); }, ); } Widget _buildMessageItem(Map message) { final imageId = message['Id']; final imageProvider = _imageCache[imageId] ?? const AssetImage('assets/default_profile_image.png'); return GestureDetector( onTap: () { if (_imageCache.containsKey(imageId)) { _showImageDialog(context, imageProvider); } }, child: Padding( padding: const EdgeInsets.symmetric(vertical: 4.0), child: Align( alignment: Alignment.centerLeft, child: Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ CircleAvatar( radius: 20, backgroundImage: imageProvider, ), const SizedBox(width: 8), Text( "${message['Name']}", style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold), ), ], ), const SizedBox(height: 4), Text( "Received at: ${message['Timestamp']}", style: const TextStyle(fontSize: 12, color: Colors.grey), ), ], ), ), ), ), ); } Color getButtonColor(String buttonStatus) { final prefsProvider = Provider.of(context); final currentStatus = prefsProvider.getCurrentStatus(); return currentStatus == buttonStatus ? Colors.blueAccent : Colors.grey; } Color getButtonTextColor(String buttonStatus) { final prefsProvider = Provider.of(context); final currentStatus = prefsProvider.getCurrentStatus(); return currentStatus == buttonStatus ? Colors.white : Colors.black; } @override Widget build(BuildContext context) { final prefsProvider = Provider.of(context); final userName = prefsProvider.getUserName(); final userLogo = prefsProvider.getUserLogo(); final userId = prefsProvider.getUserId(); return Scaffold( appBar: AppBar( backgroundColor: Colors.blueAccent, centerTitle: true, title: Stack( alignment: Alignment.center, children: [ // Menu icon in the top left Align( alignment: Alignment.centerLeft, child: CustomMenu(), ), // Centered Pogdark logo Align( alignment: Alignment.center, child: Image.asset( 'assets/pogdark_logo.png', height: 40, ), ), ], ), ), body: StreamBuilder( stream: channel.stream, builder: (context, snapshot) { if (snapshot.hasData) { final newMessage = jsonDecode(snapshot.data as String) as Map; final id = newMessage['Id']; debugPrint("Handling incoming message: $id"); _handleIncomingMessage(newMessage); } var status = prefsProvider.getCurrentStatus(); final onTheWayMessages = _getMessagesByStatus('otw'); final arrivedMessages = _getMessagesByStatus('here'); return Padding( padding: const EdgeInsets.all(8.0), child: Column( children: [ Expanded( child: Row( children: [ Expanded( child: Column( children: [ const Text( 'On the Way', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold), ), Expanded( child: ListView.builder( itemCount: onTheWayMessages.length, itemBuilder: (context, index) { return _buildMessageItem( onTheWayMessages[index]); }, ), ), ], ), ), Expanded( child: Column( children: [ const Text( 'At the Pogdark', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold), ), Expanded( child: ListView.builder( itemCount: arrivedMessages.length, itemBuilder: (context, index) { return _buildMessageItem( arrivedMessages[index]); }, ), ), ], ), ), ], ), ), Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: getButtonColor('otw'), ), onPressed: () => _sendStatus(userId, userName, userLogo, 'otw'), child: Text( 'On the Way', style: TextStyle(color: getButtonTextColor('otw')), ), ), ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: getButtonColor('here'), ), onPressed: () => _sendStatus(userId, userName, userLogo, 'here'), child: Text( 'Arrived', style: TextStyle(color: getButtonTextColor('here')), ), ), IconButton( icon: const Icon(Icons.edit, color: Colors.blueAccent), onPressed: widget.toggleProfile, tooltip: 'Edit Profile', ), if (kDebugMode) ...[ const SizedBox(height: 20), Text( status, style: TextStyle(color: Colors.red), ), ], ], ), ], ), ); }, ), ); } }