diff --git a/lib/main.dart b/lib/main.dart index 5d4a21a..4e1ec3c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,16 +1,12 @@ -import 'dart:convert'; -import 'dart:io'; -import 'dart:typed_data'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart' show ByteData, rootBundle; -import 'package:image_picker/image_picker.dart'; -import 'package:flutter/foundation.dart' show kIsWeb; -import 'package:provider/provider.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:web_socket_channel/web_socket_channel.dart'; -import 'package:uuid/uuid.dart'; import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'profile_screen.dart'; +import 'shared_preferences_provider.dart'; +import 'status_page.dart'; + void main() { runApp( ChangeNotifierProvider( @@ -59,7 +55,7 @@ class MyAppState extends State { if (snapshot.connectionState == ConnectionState.done) { if (Provider.of(context).getUserName() != '') { - return const ChatPage(); + return const StatusPage(); } else { return const ProfileScreen(isEditing: false); } @@ -71,405 +67,3 @@ class MyAppState extends State { ); } } - -class SharedPreferencesProvider extends ChangeNotifier { - late SharedPreferences prefs; - final Completer _initCompleter = Completer(); - - SharedPreferencesProvider() { - _initPrefs(); - } - - Future _initPrefs() async { - prefs = await SharedPreferences.getInstance(); - if (prefs.getString('userLogo') == null) { - ByteData bytes = await rootBundle.load('assets/default_logo.png'); - List imageBytes = bytes.buffer.asUint8List(); - prefs.setString('userLogo', base64Encode(imageBytes)); - prefs.setString('id', const Uuid().v4()); - } - _initCompleter.complete(); - notifyListeners(); - } - - Future get ready => _initCompleter.future; - - String getUserName() { - return prefs.getString('userName') ?? ''; - } - - Future setUserName(String name) async { - await prefs.setString('userName', name); - notifyListeners(); - } - - String getUserId() { - return prefs.getString('id') ?? ''; - } - - String? getUserLogo() { - final userLogo = prefs.getString('userLogo'); - return userLogo; - } - - Future setUserLogo(String? image) async { - await prefs.setString('userLogo', image!); - notifyListeners(); - } -} - -class ProfileScreen extends StatefulWidget { - final bool isEditing; - - const ProfileScreen({super.key, required this.isEditing}); - - @override - ProfileScreenState createState() => ProfileScreenState(); -} - -class ProfileScreenState extends State { - final TextEditingController _nameController = TextEditingController(); - late String? imageData; - final ImagePicker _picker = ImagePicker(); - - @override - void initState() { - super.initState(); - final prefsProvider = - Provider.of(context, listen: false); - _nameController.text = prefsProvider.getUserName(); - imageData = prefsProvider.getUserLogo(); - } - - Future _pickImage() async { - final pickedFile = await _picker.pickImage(source: ImageSource.gallery); - if (pickedFile != null) { - late final Uint8List imageBytes; - if (kIsWeb) { - imageBytes = await pickedFile.readAsBytes(); - } else { - imageBytes = await File(pickedFile.path).readAsBytes(); - } - setState(() { - if (mounted) { - final prefsProvider = - Provider.of(context, listen: false); - prefsProvider - .setUserLogo(base64Encode(imageBytes)); // Cache the image - imageData = base64Encode(imageBytes); - } - }); - } - } - - void _saveProfile() async { - String name = _nameController.text.trim(); - if (name.isNotEmpty) { - final prefsProvider = - Provider.of(context, listen: false); - await prefsProvider.setUserName(name); - await prefsProvider.setUserLogo(imageData); - - if (widget.isEditing) { - if (mounted) { - Navigator.pop(context); // Go back to the chat screen if editing - } - } else { - _navigateToChatScreen(); // Go to chat screen if this is the initial entry - } - } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Name cannot be empty!')), - ); - return; - } - } - - void _navigateToChatScreen() { - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const ChatPage()), - ); - } - - @override - void dispose() { - _nameController.dispose(); - super.dispose(); - } - - Image getLogoImage(String? logo) { - if (logo != null) { - return Image.memory(base64Decode(logo)); - } else { - return Image.asset('assets/default_logo.png'); - } - } - - @override - Widget build(BuildContext context) { - final prefsProvider = Provider.of(context); - final userLogo = prefsProvider.getUserLogo(); - - return Scaffold( - appBar: AppBar( - title: Text( - widget.isEditing ? 'Edit Profile' : 'Enter Your Name and Logo'), - backgroundColor: Colors.blueAccent, - ), - body: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // Profile Image Display - Center( - child: CircleAvatar( - radius: 50, - backgroundImage: getLogoImage(userLogo).image, - ), - ), - const SizedBox(height: 20), - TextField( - controller: _nameController, - decoration: const InputDecoration( - labelText: 'Name', - border: OutlineInputBorder(), - ), - ), - const SizedBox(height: 20), - ElevatedButton( - onPressed: _pickImage, - child: const Text('Upload Logo'), - ), - const SizedBox(height: 20), - ElevatedButton( - onPressed: _saveProfile, - child: Text(widget.isEditing ? 'Save Changes' : 'Continue'), - ), - ], - ), - ), - ); - } -} - -class ChatPage extends StatefulWidget { - const ChatPage({super.key}); - - @override - ChatPageState createState() => ChatPageState(); -} - -class ChatPageState extends State { - final WebSocketChannel channel = WebSocketChannel.connect( - Uri.parse('wss://api.pogdark.com:8889/ws'), - ); - - // Convert the stream to a broadcast stream - late final Stream broadcastStream; - late StreamController controller; - - List> messages = - []; // To hold user messages with names and timestamps - - @override - void initState() { - super.initState(); - controller = StreamController.broadcast(); - controller.addStream(channel.stream); - broadcastStream = channel.stream.asBroadcastStream(); - } - - List> _getMessagesByStatus(String status) { - return messages.where((message) => message['Status'] == status).toList(); - } - - void _navigateToEditProfile() { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const ProfileScreen(isEditing: true)), - ); - } - - void _sendStatus(String id, String name, String? image, String status) { - final message = jsonEncode({ - 'Id': id, - 'Name': name, - 'Image': image, - 'Status': status, - 'Timestamp': DateTime.now().toIso8601String(), - }); - channel.sink.add(message); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Status "$status" sent!')), - ); - } - - Widget _buildMessageItem(Map message) { - return 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: message['Image'] != null - ? Image.memory(base64Decode(message['Image'])).image - : const AssetImage('assets/default_logo.png'), - ), - 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), - ), - ], - ), - ), - ), - ); - } - - @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( - title: const Text('Pogdark'), - backgroundColor: Colors.blueAccent, - actions: [ - IconButton( - icon: const Icon(Icons.edit), - onPressed: _navigateToEditProfile, - ), - ], - ), - body: StreamBuilder( - stream: controller.stream, - builder: (context, snapshot) { - if (snapshot.hasData) { - final newMessage = - jsonDecode(snapshot.data as String) as Map; - - // Update messages only if new or modified - final incomingId = newMessage['Id']; - messages.removeWhere((msg) => msg['Id'] == incomingId); - if (newMessage['Status'] != 'expired' || - newMessage['Status'] != "is leaving") { - messages.add(newMessage); - } - } - - // Separate messages by status - final onTheWayMessages = _getMessagesByStatus('is on the way'); - final arrivedMessages = _getMessagesByStatus('has arrived'); - - return Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - children: [ - Expanded( - child: Row( - children: [ - // Column for "On the way" messages - 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]); - }, - ), - ), - ], - ), - ), - // Column for "Arrived" messages - Expanded( - child: Column( - children: [ - const Text( - 'Arrived', - 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( - onPressed: () => _sendStatus( - userId, userName, userLogo, 'is on the way'), - child: const Text('On the Way'), - ), - ElevatedButton( - onPressed: () => _sendStatus( - userId, userName, userLogo, 'has arrived'), - child: const Text('Arrived'), - ), - ElevatedButton( - onPressed: () => - _sendStatus(userId, userName, userLogo, 'is leaving'), - child: const Text('Leaving'), - ), - ], - ), - ], - ), - ); - }, - ), - ); - } - - @override - void dispose() { - channel.sink.close(); - super.dispose(); - } -} diff --git a/lib/profile_screen.dart b/lib/profile_screen.dart new file mode 100644 index 0000000..1685a83 --- /dev/null +++ b/lib/profile_screen.dart @@ -0,0 +1,147 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:provider/provider.dart'; + +import 'shared_preferences_provider.dart'; +import 'status_page.dart'; + +class ProfileScreen extends StatefulWidget { + final bool isEditing; + + const ProfileScreen({super.key, required this.isEditing}); + + @override + ProfileScreenState createState() => ProfileScreenState(); +} + +class ProfileScreenState extends State { + final TextEditingController _nameController = TextEditingController(); + late String? imageData; + final ImagePicker _picker = ImagePicker(); + + @override + void initState() { + super.initState(); + final prefsProvider = + Provider.of(context, listen: false); + _nameController.text = prefsProvider.getUserName(); + imageData = prefsProvider.getUserLogo(); + } + + Future _pickImage() async { + final pickedFile = await _picker.pickImage(source: ImageSource.gallery); + if (pickedFile != null) { + late final Uint8List imageBytes; + if (kIsWeb) { + imageBytes = await pickedFile.readAsBytes(); + } else { + imageBytes = await File(pickedFile.path).readAsBytes(); + } + setState(() { + if (mounted) { + final prefsProvider = + Provider.of(context, listen: false); + prefsProvider + .setUserLogo(base64Encode(imageBytes)); // Cache the image + imageData = base64Encode(imageBytes); + } + }); + } + } + + void _saveProfile() async { + String name = _nameController.text.trim(); + if (name.isNotEmpty) { + final prefsProvider = + Provider.of(context, listen: false); + await prefsProvider.setUserName(name); + await prefsProvider.setUserLogo(imageData); + + if (widget.isEditing) { + if (mounted) { + Navigator.pop(context); // Go back to the Status screen if editing + } + } else { + _navigateToStatusScreen(); // Go to Status screen if this is the initial entry + } + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Name cannot be empty!')), + ); + return; + } + } + + void _navigateToStatusScreen() { + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (context) => const StatusPage()), + ); + } + + @override + void dispose() { + _nameController.dispose(); + super.dispose(); + } + + Image getLogoImage(String? logo) { + if (logo != null) { + return Image.memory(base64Decode(logo)); + } else { + return Image.asset('assets/default_logo.png'); + } + } + + @override + Widget build(BuildContext context) { + final prefsProvider = Provider.of(context); + final userLogo = prefsProvider.getUserLogo(); + + return Scaffold( + appBar: AppBar( + title: Text( + widget.isEditing ? 'Edit Profile' : 'Enter Your Name and Logo'), + backgroundColor: Colors.blueAccent, + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Profile Image Display + Center( + child: CircleAvatar( + radius: 50, + backgroundImage: getLogoImage(userLogo).image, + ), + ), + const SizedBox(height: 20), + TextField( + controller: _nameController, + decoration: const InputDecoration( + labelText: 'Name', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: _pickImage, + child: const Text('Upload Logo'), + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: _saveProfile, + child: Text(widget.isEditing ? 'Save Changes' : 'Continue'), + ), + ], + ), + ), + ); + } +} diff --git a/lib/shared_preferences_provider.dart b/lib/shared_preferences_provider.dart new file mode 100644 index 0000000..90d7b99 --- /dev/null +++ b/lib/shared_preferences_provider.dart @@ -0,0 +1,52 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:flutter/services.dart' show ByteData, rootBundle; +import 'package:uuid/uuid.dart'; + +class SharedPreferencesProvider extends ChangeNotifier { + late SharedPreferences prefs; + final Completer _initCompleter = Completer(); + + SharedPreferencesProvider() { + _initPrefs(); + } + + Future _initPrefs() async { + prefs = await SharedPreferences.getInstance(); + if (prefs.getString('userLogo') == null) { + ByteData bytes = await rootBundle.load('assets/default_logo.png'); + List imageBytes = bytes.buffer.asUint8List(); + prefs.setString('userLogo', base64Encode(imageBytes)); + prefs.setString('id', const Uuid().v4()); + } + _initCompleter.complete(); + notifyListeners(); + } + + Future get ready => _initCompleter.future; + + String getUserName() { + return prefs.getString('userName') ?? ''; + } + + Future setUserName(String name) async { + await prefs.setString('userName', name); + notifyListeners(); + } + + String getUserId() { + return prefs.getString('id') ?? ''; + } + + String? getUserLogo() { + final userLogo = prefs.getString('userLogo'); + return userLogo; + } + + Future setUserLogo(String? image) async { + await prefs.setString('userLogo', image!); + notifyListeners(); + } +} diff --git a/lib/status_page.dart b/lib/status_page.dart new file mode 100644 index 0000000..d8452f2 --- /dev/null +++ b/lib/status_page.dart @@ -0,0 +1,229 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; + +import 'profile_screen.dart'; +import 'shared_preferences_provider.dart'; + +class StatusPage extends StatefulWidget { + const StatusPage({super.key}); + + @override + StatusPageState createState() => StatusPageState(); +} + +class StatusPageState extends State { + final WebSocketChannel channel = WebSocketChannel.connect( + Uri.parse('wss://api.pogdark.com:8889/ws'), + ); + + // Convert the stream to a broadcast stream + late final Stream broadcastStream; + late StreamController controller; + + List> messages = + []; // To hold user messages with names and timestamps + + @override + void initState() { + super.initState(); + controller = StreamController.broadcast(); + controller.addStream(channel.stream); + broadcastStream = channel.stream.asBroadcastStream(); + } + + List> _getMessagesByStatus(String status) { + return messages.where((message) => message['Status'] == status).toList(); + } + + void _navigateToEditProfile() { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const ProfileScreen(isEditing: true)), + ); + } + + void _sendStatus(String id, String name, String? image, String status) { + final message = jsonEncode({ + 'Id': id, + 'Name': name, + 'Image': image, + 'Status': status, + 'Timestamp': DateTime.now().toIso8601String(), + }); + channel.sink.add(message); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Status "$status" sent!')), + ); + } + + Widget _buildMessageItem(Map message) { + return 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: message['Image'] != null + ? Image.memory(base64Decode(message['Image'])).image + : const AssetImage('assets/default_logo.png'), + ), + 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), + ), + ], + ), + ), + ), + ); + } + + @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( + title: const Text('Pogdark'), + backgroundColor: Colors.blueAccent, + actions: [ + IconButton( + icon: const Icon(Icons.edit), + onPressed: _navigateToEditProfile, + ), + ], + ), + body: StreamBuilder( + stream: controller.stream, + builder: (context, snapshot) { + if (snapshot.hasData) { + final newMessage = + jsonDecode(snapshot.data as String) as Map; + + // Update messages only if new or modified + final incomingId = newMessage['Id']; + messages.removeWhere((msg) => msg['Id'] == incomingId); + if (newMessage['Status'] != 'expired' || + newMessage['Status'] != "is leaving") { + messages.add(newMessage); + } + } + + // Separate messages by status + final onTheWayMessages = _getMessagesByStatus('is on the way'); + final arrivedMessages = _getMessagesByStatus('has arrived'); + + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Expanded( + child: Row( + children: [ + // Column for "On the way" messages + 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]); + }, + ), + ), + ], + ), + ), + // Column for "Arrived" messages + Expanded( + child: Column( + children: [ + const Text( + 'Arrived', + 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( + onPressed: () => _sendStatus( + userId, userName, userLogo, 'is on the way'), + child: const Text('On the Way'), + ), + ElevatedButton( + onPressed: () => _sendStatus( + userId, userName, userLogo, 'has arrived'), + child: const Text('Arrived'), + ), + ElevatedButton( + onPressed: () => + _sendStatus(userId, userName, userLogo, 'is leaving'), + child: const Text('Leaving'), + ), + ], + ), + ], + ), + ); + }, + ), + ); + } + + @override + void dispose() { + channel.sink.close(); + super.dispose(); + } +}