import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:huacu_mobile/models/auth_data.dart'; import 'package:huacu_mobile/models/chat_entry.dart'; import 'package:huacu_mobile/models/game.dart'; import 'package:huacu_mobile/ui/widgets/color_plate.dart'; import 'package:huacu_mobile/ui/widgets/static_color_plate.dart'; import 'package:socket_io_client/socket_io_client.dart' as io; 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 void dispose() { socket.off("hello"); socket.off("gameStatus"); socket.off("leaveGameResponse"); socket.off("chatResponse"); socket.off("guessResponse"); socket.off("guess"); super.dispose(); } @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("${guessedColors.length}/${gameInfo.tries}")), ), ), ], ), ), ), ); } 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), ), ], ), ), ], ); } }