All checks were successful
Build Flutter Web and Docker Image for Local Registry / Build Flutter Web App (push) Successful in 3m22s
485 lines
16 KiB
Dart
485 lines
16 KiB
Dart
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<StatusPage> with WidgetsBindingObserver {
|
|
late final WebSocketChannel channel;
|
|
List<Map<String, dynamic>> messages = [];
|
|
final Map<String, ImageProvider> _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<SharedPreferencesProvider>(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<String, dynamic>;
|
|
|
|
// 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<dynamic>) {
|
|
debugPrint("Messages to update: ${parsed.length}");
|
|
// Ensure each item is a Map<String, dynamic>
|
|
final List<Map<String, dynamic>> jsonResponse = parsed
|
|
.where((item) => item is Map<String, dynamic>)
|
|
.map((item) => item as Map<String, dynamic>)
|
|
.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<Map<String, dynamic>> _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<SharedPreferencesProvider>(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<String, dynamic> message) async {
|
|
final prefsProvider =
|
|
Provider.of<SharedPreferencesProvider>(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<String, dynamic> 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<SharedPreferencesProvider>(context);
|
|
final currentStatus = prefsProvider.getCurrentStatus();
|
|
return currentStatus == buttonStatus ? Colors.blueAccent : Colors.grey;
|
|
}
|
|
|
|
Color getButtonTextColor(String buttonStatus) {
|
|
final prefsProvider = Provider.of<SharedPreferencesProvider>(context);
|
|
final currentStatus = prefsProvider.getCurrentStatus();
|
|
return currentStatus == buttonStatus ? Colors.white : Colors.black;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final prefsProvider = Provider.of<SharedPreferencesProvider>(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<String, dynamic>;
|
|
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),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|