import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:huacu_mobile/color_plate.dart'; import 'package:huacu_mobile/game_pallete.dart'; import 'package:huacu_mobile/socket_connection_indicator.dart'; import 'package:huacu_mobile/static_color_plate.dart'; import 'package:socket_io_client/socket_io_client.dart' as io; final darkTheme = ThemeData( useMaterial3: true, primarySwatch: Colors.amber, brightness: Brightness.dark, ); void main() { runApp(GetMaterialApp( initialRoute: '/auth', title: 'HUACU', darkTheme: darkTheme, themeMode: ThemeMode.dark, defaultTransition: Transition.native, getPages: [ GetPage(name: '/auth', page: () => const AuthPage()), GetPage(name: '/home', page: () => const HomePage()), GetPage(name: '/game', page: () => const GamePage()), GetPage(name: '/testing', page: () => const TestingGroundsPage()), ], )); } class TestingGroundsPage extends StatefulWidget { const TestingGroundsPage({super.key}); @override State createState() => _TestingGroundsPageState(); } class _TestingGroundsPageState extends State { final testInt = 5.obs; @override Widget build(BuildContext context) { final size = MediaQuery.of(context).size; return Scaffold( body: SizedBox( width: size.width, height: size.height, child: Stack( children: [ InteractiveViewer( constrained: false, boundaryMargin: const EdgeInsets.all(800), maxScale: 0.8, minScale: 0.4, child: _generateGameField(), ), Positioned( bottom: 0, right: 0, child: InkResponse( onTap: _openChat, child: Container( width: 50, height: 50, decoration: const BoxDecoration( color: Colors.amberAccent, borderRadius: BorderRadius.only(topLeft: Radius.circular(25)), ), child: const Icon( Icons.message, color: Colors.black, ), ), ), ), Positioned( top: 0, right: 0, child: InkResponse( onTap: _leaveGame, child: Container( width: 50, height: 50, decoration: const BoxDecoration( color: Colors.redAccent, borderRadius: BorderRadius.only(bottomLeft: Radius.circular(25)), ), child: const Icon( Icons.close, color: Colors.black, ), ), ), ), Positioned( bottom: 0, left: 0, child: InkResponse( onTap: _openColorsCard, child: Container( width: 50, height: 50, decoration: const BoxDecoration( color: Colors.greenAccent, borderRadius: BorderRadius.only(topRight: Radius.circular(25)), ), child: const Icon( Icons.content_copy_rounded, color: Colors.black, ), ), ), ), Positioned( top: 0, left: 0, child: Container( height: 50, width: 50, decoration: const BoxDecoration( color: Colors.black, borderRadius: BorderRadius.only(bottomRight: Radius.circular(25)), ), child: Padding( padding: const EdgeInsets.all(8.0), child: Obx(() => Text( "${10 - testInt.value}/10", )), ), ), ), ], ), ), ); } Widget _generateGameField() { const alphabet = "ABCDEFGHIJKLMNOPQRST"; return Container( width: (alphabet.length + 3) * 100, height: 1300, padding: const EdgeInsets.all(10), child: Stack( children: [ for (int i = 0; i < alphabet.length; i++) for (int j = 0; j < 10; j++) Positioned( left: (i + 1) * 100.0, top: (j + 1) * 100.0, child: ColorPlate( plateName: "${alphabet[i]}$j", size: 90, onSelected: _onPlateSelected, ), ), // Now I need to generate letters and numbers around field for (int i = 0; i < alphabet.length; i++) Positioned( left: (i + 1) * 100.0, top: 0, child: Container( width: 90, height: 90, alignment: Alignment.center, child: Text(alphabet[i], style: const TextStyle(fontSize: 40)), ), ), for (int i = 0; i < alphabet.length; i++) Positioned( left: (i + 1) * 100.0, top: 1100, child: Container( width: 90, height: 90, alignment: Alignment.center, child: Text(alphabet[i], style: const TextStyle(fontSize: 40)), ), ), for (int j = 0; j < 10; j++) Positioned( left: 0, top: (j + 1) * 100.0, child: Container( width: 90, height: 90, alignment: Alignment.center, child: Text("$j", style: const TextStyle(fontSize: 40)), ), ), for (int j = 0; j < 10; j++) Positioned( left: (alphabet.length + 1) * 100, top: (j + 1) * 100.0, child: Container( width: 90, height: 90, alignment: Alignment.center, child: Text("$j", style: const TextStyle(fontSize: 40)), ), ), ], ), ); } void _onPlateSelected(String name) { Get.defaultDialog( title: "Plate selected", middleText: "Plate $name was selected", barrierDismissible: false, textConfirm: "OK", onConfirm: () => Get.back(), ); } void _openChat() { Get.bottomSheet(BottomSheet( onClosing: () {}, clipBehavior: Clip.antiAliasWithSaveLayer, builder: (context) => Container( height: 300, child: _buildBottomSheetChatContent(), ), )); } void _leaveGame() { Get.defaultDialog( title: "Leave game", middleText: "Are you sure you want to leave the game?", barrierDismissible: false, textConfirm: "Yes", textCancel: "No", onConfirm: () => Get.offAllNamed('/home'), onCancel: () => Get.back(), ); } void _openColorsCard() { final randomColors = pallete.keys.toList(growable: false); randomColors.shuffle(); Get.dialog(AlertDialog( title: const Text("Colors card"), content: SizedBox( width: 300, child: GridView.builder( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, ), itemCount: 6, itemBuilder: (context, index) { final randomColor = randomColors[index]; return Padding( padding: const EdgeInsets.all(2), child: Column( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ StaticColorPlate( plateName: randomColor, marked: false, size: 90, ), const SizedBox(height: 10), Text( randomColor, style: const TextStyle( fontSize: 12, fontStyle: FontStyle.italic, ), ), ], ), ); }, ), ), actions: [ TextButton( onPressed: () => Get.back(), child: const Text("OK"), ), ], )); } Widget _buildBottomSheetChatContent() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: ListView.builder( itemCount: 10, itemBuilder: (context, index) { return const Card( child: ListTile( title: Text("asdasdasdasd"), subtitle: Text("from someone"), subtitleTextStyle: TextStyle(fontStyle: FontStyle.italic, fontSize: 12), ), ); }, ), ), Padding( padding: const EdgeInsets.all(16), child: Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, children: [ const Expanded( child: TextField( decoration: InputDecoration( enabledBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.grey, width: 0.5), borderRadius: BorderRadius.all(Radius.circular(32)), ), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.amber, width: 2.0), borderRadius: BorderRadius.all(Radius.circular(32)), ), hintText: "Your message", ), ), ), IconButton( onPressed: () {}, icon: const Icon(Icons.send), ), ], ), ), ], ); } } class AuthData { final String login; final String password; const AuthData(this.login, this.password); } class AuthPage extends StatefulWidget { const AuthPage({super.key}); @override State createState() => _AuthPageState(); } class _AuthPageState extends State { late io.Socket socket; TextEditingController loginController = TextEditingController(); TextEditingController passwordController = TextEditingController(); @override void initState() { super.initState(); const serverUrl = kDebugMode ? "http://localhost:9800" : "https://huacu.nuark.xyz"; socket = io.io(serverUrl, { "transports": ["websocket"], }); } @override Widget build(BuildContext context) { final size = MediaQuery.of(context).size; double? formWidth = size.width * 0.5; if (formWidth < 300) { formWidth = null; } return Scaffold( body: Center( child: Container( width: formWidth, padding: const EdgeInsets.all(16), child: FocusTraversalGroup( policy: OrderedTraversalPolicy(), child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "HUACU", style: Theme.of(context).textTheme.displayLarge, ), SocketConnectionIndicator(socket: socket), ], ), const SizedBox(height: 32), TextField( controller: loginController, decoration: const InputDecoration( border: OutlineInputBorder(), labelText: "Login", ), ), const SizedBox(height: 16), TextField( controller: passwordController, decoration: const InputDecoration( border: OutlineInputBorder(), labelText: "Password", ), ), const SizedBox(height: 16), Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.end, children: [ ElevatedButton( onPressed: _handleLoginClick, child: const Text("Login"), ), const SizedBox(width: 16), ElevatedButton( onPressed: _handleRegistrationClick, child: const Text("Register"), ), ], ), ], ), ), ), ), ); } void _loginEventHandler(dynamic data) { try { bool ok = data[0]; String message = data[1]; Get.snackbar( ok ? "Login successful" : "Login unsuccessful", message, ); if (ok) { Get.put(AuthData(loginController.text, passwordController.text)); Get.put(socket); Get.offNamed("/home"); } } catch (e) { Get.snackbar("Error", e.toString()); } socket.off("login", _loginEventHandler); } void _handleLoginClick() { socket.on("login", _loginEventHandler); socket.emit( "login", [loginController.text, passwordController.text], ); } void _registerEventHandler(dynamic data) { try { bool ok = data[0]; String message = data[1]; Get.snackbar( ok ? "Registration successful" : "Registration unsuccessful", message, ); if (ok) { _handleLoginClick(); } } catch (e) { Get.snackbar("Error", e.toString()); } socket.off("register", _registerEventHandler); } void _handleRegistrationClick() { socket.on("register", _registerEventHandler); socket.emit( "register", [loginController.text, passwordController.text], ); } } class Game { final String id; final String player1; final String player2; final Set colors; Game(this.id, this.player1, this.player2, this.colors); } class HomePage extends StatefulWidget { const HomePage({super.key}); @override State createState() => _HomePageState(); } class _HomePageState extends State { final io.Socket socket = Get.find(); final AuthData authData = Get.find(); var guessersPlayers = [].obs; var suggestersPlayers = [].obs; var incommingRequests = [].obs; @override void initState() { super.initState(); socket.on("hello", (idky) { socket.dispose(); Get.offAllNamed("/auth"); }); socket.on("update", (update) { bool ok = update[0]; if (ok) { var data = update[1]; guessersPlayers.value = (data["guessers"] as List? ?? []) .map((e) => e.toString()) .toList(growable: false); suggestersPlayers.value = (data["suggesters"] as List? ?? []) .map((e) => e.toString()) .toList(growable: false); } else { Get.snackbar("Error", "Update failed with message: ${update[1]}"); } }); socket.on("updateNeeded", (data) { socket.emit("getUpdate"); }); socket.on("gameRequest", (requestFrom) { setState(() { incommingRequests.add(requestFrom); }); }); socket.on("requestResponseResult", (data) { bool ok = data[0]; if (ok) { socket.off("hello"); socket.off("update"); socket.off("updateNeeded"); socket.off("gameRequest"); socket.off("requestResponseResult"); Get.put(authData); Get.put(socket); Get.put(Game( data[1]["id"], data[1]["player1"], data[1]["player2"], (data[2] as List).map((e) => e.toString()).toSet(), )); Get.offNamed("/game"); } else { Get.snackbar("Request response", data[1]); } }); socket.emit("getUpdate"); } @override Widget build(BuildContext context) { final shortestSide = MediaQuery.of(context).size.shortestSide; final useMobileLayout = shortestSide < 600; final elements = [ Expanded( child: Padding( padding: const EdgeInsets.all(16), child: _buildPlayerList( guessersPlayers, "Guessers", "guessers", ), ), ), Expanded( child: Padding( padding: const EdgeInsets.all(16), child: _buildPlayerList( suggestersPlayers, "Suggesters", "suggesters", ), ), ), ]; return Scaffold( appBar: AppBar( title: Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text("HUACU"), SocketConnectionIndicator(socket: socket, size: 8), ], ), ), body: useMobileLayout ? Column( mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.start, children: elements, ) : Row( mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.start, children: elements, ), ); } Widget _buildPlayerList(RxList players, String title, String team) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title), Obx(() => Text("Count: ${players.length}")), Expanded( child: ObxValue( (data) => ListView.builder( itemCount: data.length, itemBuilder: (context, index) { final username = data[index]; final hasRequest = incommingRequests.contains(username); final you = authData.login == username; return Card( child: ListTile( title: Text( "$username ${you ? "(you)" : ""}".trim(), ), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ if (hasRequest) InkResponse( onTap: () => _handleRequestResponseAction(username, true), child: const Icon(Icons.check), ), if (hasRequest) InkResponse( onTap: () => _handleRequestResponseAction(username, false), child: const Icon(Icons.close), ), if (!you && !hasRequest) InkResponse( onTap: () => _handleSendRequestClick(username), child: const Icon(Icons.connect_without_contact), ), ], ), ), ); }, ), players, ), ), Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, children: [ Expanded( child: ObxValue( (data) => players.contains(authData.login) ? ElevatedButton( onPressed: () => _handleLeaveClick(team), child: const Text("Leave"), ) : ElevatedButton( onPressed: () => _handleJoinClick(team), child: const Text("Join"), ), guessersPlayers, ), ), ], ), ], ); } void _joinResponseHandler(dynamic data) { bool ok = data[0]; if (!ok) { int status = data[1]; Get.snackbar("Error", "Join failed with status $status"); } socket.off("joinResponse", _joinResponseHandler); } void _handleJoinClick(String side) { socket.on("joinResponse", _joinResponseHandler); socket.emit("join", side); } void _leaveResponseHandler(dynamic data) { bool ok = data[0]; if (!ok) { int status = data[1]; Get.snackbar("Error", "Leaving failed with status $status"); } socket.off("leaveResponse", _leaveResponseHandler); } void _handleLeaveClick(String side) { socket.on("leaveResponse", _leaveResponseHandler); socket.emit("leave", side); } void _sendRequestResponseHandler(dynamic data) { bool ok = data[0]; if (ok) { Get.snackbar("Success", "Request sent"); } else { Get.snackbar("Error", "Request failed"); } socket.off("sendRequestResponse", _leaveResponseHandler); } void _handleSendRequestClick(String player) { socket.on("sendRequestResponse", _sendRequestResponseHandler); socket.emit("sendRequest", player); } void _handleRequestResponseAction(String data, bool response) { socket.emit("requestResponse", [data, response]); setState(() { incommingRequests.remove(data); }); } } class ChatEntry { final String from; final String message; ChatEntry(this.from, this.message); } class GamePage extends StatefulWidget { const GamePage({Key? key}) : super(key: key); @override State createState() => _GamePageState(); } class _GamePageState extends State { final io.Socket socket = Get.find(); final AuthData authData = Get.find(); final Game gameInfo = Get.find(); bool get isGuesser => gameInfo.colors.isEmpty; var chat = [].obs; var guessedColors = {}.obs; final _chatController = TextEditingController(); final _scrollController = ScrollController(); @override void initState() { super.initState(); socket.on("hello", (idky) { socket.dispose(); Get.offAllNamed("/auth"); }); socket.on("gameStatus", (data) { bool won = data[0]; Get.defaultDialog( title: "Game ended", middleText: won ? "You won!" : "You lost!", barrierDismissible: false, onConfirm: () { Get.put(authData); Get.put(socket); Get.offAllNamed("/home"); }, ); }); socket.on("leaveGameResponse", (data) { if (data[0] == true && data[1] == 410) { Get.defaultDialog( title: "Game ended", middleText: "Your opponent left the game", barrierDismissible: false, onConfirm: () { Get.put(authData); Get.put(socket); Get.offAllNamed("/home"); }, ); } else { Get.put(authData); Get.put(socket); Get.offAllNamed("/home"); } }); socket.on("chatResponse", (data) { bool ok = data[0]; if (ok) { chat.add(ChatEntry(data[1]["from"], data[1]["message"])); } }); if (isGuesser) { socket.on("guessResponse", _onGuessResponse); } else { socket.on("guess", (data) { setState(() { guessedColors.add(data); }); }); } chat.listen((data) { _scrollToEnd(); _showMessageNotification(data.last); }); } @override Widget build(BuildContext context) { final size = MediaQuery.of(context).size; return SafeArea( child: Scaffold( body: SizedBox( width: size.width, height: size.height, child: Stack( children: [ InteractiveViewer( constrained: false, boundaryMargin: const EdgeInsets.all(800), maxScale: 0.8, minScale: 0.4, child: isGuesser ? _generateGameField() : _generateStaticField(), ), Positioned( bottom: 0, right: 0, child: InkResponse( onTap: _openChat, child: Container( width: 50, height: 50, decoration: const BoxDecoration( color: Colors.amberAccent, borderRadius: BorderRadius.only(topLeft: Radius.circular(25)), ), child: const Icon( Icons.message, color: Colors.black, ), ), ), ), Positioned( top: 0, right: 0, child: InkResponse( onTap: _leaveGame, child: Container( width: 50, height: 50, decoration: const BoxDecoration( color: Colors.redAccent, borderRadius: BorderRadius.only(bottomLeft: Radius.circular(25)), ), child: const Icon( Icons.close, color: Colors.black, ), ), ), ), if (!isGuesser) Positioned( bottom: 0, left: 0, child: InkResponse( onTap: _openColorsCard, child: Container( width: 50, height: 50, decoration: const BoxDecoration( color: Colors.greenAccent, borderRadius: BorderRadius.only(topRight: Radius.circular(25)), ), child: const Icon( Icons.content_copy_rounded, color: Colors.black, ), ), ), ), Positioned( top: 0, left: 0, child: Container( height: 50, width: 50, alignment: Alignment.center, decoration: const BoxDecoration( color: Colors.black, borderRadius: BorderRadius.only(bottomRight: Radius.circular(25)), ), child: Obx(() => Text("${10 - guessedColors.length}/10")), ), ), ], ), ), ), ); } Widget _generateGameField() { const alphabet = "ABCDEFGHIJKLMNOPQRST"; return Container( width: (alphabet.length + 3) * 100, height: 1300, padding: const EdgeInsets.all(10), child: Stack( children: [ for (int i = 0; i < alphabet.length; i++) for (int j = 0; j < 10; j++) Positioned( left: (i + 1) * 100.0, top: (j + 1) * 100.0, child: ColorPlate( plateName: "${alphabet[i]}$j", size: 90, onSelected: _onPlateSelected, ), ), // Now I need to generate letters and numbers around field for (int i = 0; i < alphabet.length; i++) Positioned( left: (i + 1) * 100.0, top: 0, child: Container( width: 90, height: 90, alignment: Alignment.center, child: Text(alphabet[i], style: const TextStyle(fontSize: 40)), ), ), for (int i = 0; i < alphabet.length; i++) Positioned( left: (i + 1) * 100.0, top: 1100, child: Container( width: 90, height: 90, alignment: Alignment.center, child: Text(alphabet[i], style: const TextStyle(fontSize: 40)), ), ), for (int j = 0; j < 10; j++) Positioned( left: 0, top: (j + 1) * 100.0, child: Container( width: 90, height: 90, alignment: Alignment.center, child: Text("$j", style: const TextStyle(fontSize: 40)), ), ), for (int j = 0; j < 10; j++) Positioned( left: (alphabet.length + 1) * 100, top: (j + 1) * 100.0, child: Container( width: 90, height: 90, alignment: Alignment.center, child: Text("$j", style: const TextStyle(fontSize: 40)), ), ), ], ), ); } Widget _generateStaticField() { const alphabet = "ABCDEFGHIJKLMNOPQRST"; return Container( width: (alphabet.length + 3) * 100, height: 1300, padding: const EdgeInsets.all(10), child: Stack( children: [ for (int i = 0; i < alphabet.length; i++) for (int j = 0; j < 10; j++) Positioned( left: (i + 1) * 100.0, top: (j + 1) * 100.0, child: StaticColorPlate( plateName: "${alphabet[i]}$j", size: 90, marked: guessedColors.contains("${alphabet[i]}$j"), ), ), // Now I need to generate letters and numbers around field for (int i = 0; i < alphabet.length; i++) Positioned( left: (i + 1) * 100.0, top: 0, child: Container( width: 90, height: 90, alignment: Alignment.center, child: Text(alphabet[i], style: const TextStyle(fontSize: 40)), ), ), for (int i = 0; i < alphabet.length; i++) Positioned( left: (i + 1) * 100.0, top: 1100, child: Container( width: 90, height: 90, alignment: Alignment.center, child: Text(alphabet[i], style: const TextStyle(fontSize: 40)), ), ), for (int j = 0; j < 10; j++) Positioned( left: 0, top: (j + 1) * 100.0, child: Container( width: 90, height: 90, alignment: Alignment.center, child: Text("$j", style: const TextStyle(fontSize: 40)), ), ), for (int j = 0; j < 10; j++) Positioned( left: (alphabet.length + 1) * 100, top: (j + 1) * 100.0, child: Container( width: 90, height: 90, alignment: Alignment.center, child: Text("$j", style: const TextStyle(fontSize: 40)), ), ), ], ), ); } void _onPlateSelected(String name) { guessedColors.add(name); socket.emit("guess", [gameInfo.id, name]); } void _onGuessResponse(dynamic data) { bool ok = data[0]; if (ok) { Get.snackbar("Success", "Guess sent successfully"); } else { Get.snackbar("Error", "Guess failed to send"); } } void _openChat() { Get.bottomSheet(BottomSheet( onClosing: () {}, clipBehavior: Clip.antiAliasWithSaveLayer, builder: (context) => SizedBox( height: 300, child: _buildBottomSheetChatContent(), ), )); _scrollToEnd(); } void _leaveGame() { Get.defaultDialog( title: "Leave game", middleText: "Are you sure you want to leave the game?", barrierDismissible: false, textConfirm: "Yes", textCancel: "No", onConfirm: () { socket.emit("leaveGame", [gameInfo.id]); }, onCancel: () => Get.back(), ); } void _openColorsCard() { Get.dialog(AlertDialog( title: const Text("Colors card"), content: SizedBox( width: 300, child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ GridView.builder( shrinkWrap: true, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, ), itemCount: 6, itemBuilder: (context, index) { final color = gameInfo.colors.elementAt(index); return Padding( padding: const EdgeInsets.all(2), child: Column( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ Obx( () => StaticColorPlate( plateName: color, marked: guessedColors.contains(color), size: 90, ), ), const SizedBox(height: 10), Text( gameInfo.colors.elementAt(index), style: const TextStyle( fontSize: 12, fontStyle: FontStyle.italic, ), ), ], ), ); }, ), ], ), ), ), actions: [ TextButton( onPressed: () => Get.back(), child: const Text("OK"), ), ], )); } void _scrollToEnd() { WidgetsBinding.instance.addPostFrameCallback((_) { if (_scrollController.hasClients) { _scrollController.animateTo( _scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: 300), curve: Curves.easeOut, ); } }); } void _showMessageNotification(ChatEntry entry) async { if (Get.isBottomSheetOpen == true) return; await Get.closeCurrentSnackbar(); Get.snackbar( entry.message, "says ${entry.from}", backgroundColor: Colors.black, borderColor: Colors.amber, borderWidth: 2, margin: const EdgeInsets.all(16), duration: const Duration(seconds: 5), animationDuration: const Duration(milliseconds: 300), ); } Widget _buildBottomSheetChatContent() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: ObxValue( (data) => ListView.builder( controller: _scrollController, itemCount: data.length, itemBuilder: (context, index) { return Card( child: ListTile( title: Text(data[index].message), subtitle: Text("from ${data[index].from}"), subtitleTextStyle: const TextStyle( fontStyle: FontStyle.italic, fontSize: 12), ), ); }, ), chat, ), ), Padding( padding: const EdgeInsets.all(16), child: Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, children: [ Expanded( child: TextField( controller: _chatController, onSubmitted: (value) { final text = _chatController.text; if (text.isNotEmpty) { socket.emit("chat", [gameInfo.id, text.trim()]); } _chatController.clear(); }, decoration: const InputDecoration( enabledBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.grey, width: 0.5), borderRadius: BorderRadius.all(Radius.circular(32)), ), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.amber, width: 2.0), borderRadius: BorderRadius.all(Radius.circular(32)), ), hintText: "Your message", ), ), ), IconButton( onPressed: () { final text = _chatController.text; if (text.isNotEmpty) { socket.emit("chat", [gameInfo.id, text.trim()]); } _chatController.clear(); }, icon: const Icon(Icons.send), ), ], ), ), ], ); } }