diff --git a/lib/color_plate.dart b/lib/color_plate.dart new file mode 100644 index 0000000..4a13e69 --- /dev/null +++ b/lib/color_plate.dart @@ -0,0 +1,108 @@ +import 'package:flutter/material.dart'; + +import 'game_pallete.dart'; + +enum PlateState { + normal, + marked, + selected, +} + +typedef PlateMarkCallback = void Function(String plateName); + +class ColorPlate extends StatefulWidget { + final String plateName; + final double size; + final PlateMarkCallback? onSelected; + + const ColorPlate( + {super.key, + required this.plateName, + required this.size, + this.onSelected}); + + @override + State createState() => _ColorPlateState(); +} + +class _ColorPlateState extends State { + var _tapPosition = Offset.zero; + var state = PlateState.normal; + + Color get borderColor { + switch (state) { + case PlateState.normal: return Colors.black; + case PlateState.marked: return Colors.amber; + case PlateState.selected: return Colors.greenAccent; + } + } + + void _getTapPosition(TapDownDetails details) { + final RenderBox referenceBox = context.findRenderObject() as RenderBox; + _tapPosition = referenceBox.globalToLocal(details.globalPosition); + _tapPosition = details.globalPosition; + } + + void _showContextMenu(BuildContext context) async { + final oldState = state; + setState(() { + state = PlateState.marked; + }); + + final overlay = Overlay.of(context).context.findRenderObject(); + final result = await showMenu( + context: context, + position: RelativeRect.fromRect( + Rect.fromLTWH(_tapPosition.dx, _tapPosition.dy, 30, 30), + Rect.fromLTWH(0, 0, overlay!.paintBounds.size.width, + overlay.paintBounds.size.height), + ), + items: [ + PopupMenuItem( + enabled: false, + child: Text("Plate ${widget.plateName}"), + ), + PopupMenuItem( + enabled: oldState != PlateState.marked, + value: "mark", + child: Text("Mark"), + ), + const PopupMenuItem( + value: "cancel", + child: Text("Cancel"), + ) + ], + ); + + setState(() { + if (result == "mark" || oldState == PlateState.selected) { + state = PlateState.selected; + if (oldState != PlateState.selected) { + widget.onSelected?.call(widget.plateName); + } + } else { + state = PlateState.normal; + } + }); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTapDown: (details) => _getTapPosition(details), + onTap: () => _showContextMenu(context), + child: Container( + width: widget.size, + height: widget.size, + decoration: BoxDecoration( + color: pallete[widget.plateName], + border: Border.all( + color: borderColor, + width: 4, + ), + borderRadius: BorderRadius.circular(10), + ), + ), + ); + } +} diff --git a/lib/game_pallete.dart b/lib/game_pallete.dart new file mode 100644 index 0000000..d0f09dd --- /dev/null +++ b/lib/game_pallete.dart @@ -0,0 +1,204 @@ +import 'dart:ui'; + +const pallete = { + "A0": Color.fromRGBO(0, 0, 186, 1), + "A1": Color.fromRGBO(0, 26, 186, 1), + "A2": Color.fromRGBO(0, 51, 186, 1), + "A3": Color.fromRGBO(0, 77, 186, 1), + "A4": Color.fromRGBO(0, 102, 186, 1), + "A5": Color.fromRGBO(0, 128, 186, 1), + "A6": Color.fromRGBO(0, 153, 186, 1), + "A7": Color.fromRGBO(0, 179, 186, 1), + "A8": Color.fromRGBO(0, 204, 186, 1), + "A9": Color.fromRGBO(0, 230, 186, 1), + "B0": Color.fromRGBO(13, 0, 186, 1), + "B1": Color.fromRGBO(13, 26, 186, 1), + "B2": Color.fromRGBO(13, 51, 186, 1), + "B3": Color.fromRGBO(13, 77, 186, 1), + "B4": Color.fromRGBO(13, 102, 186, 1), + "B5": Color.fromRGBO(13, 128, 186, 1), + "B6": Color.fromRGBO(13, 153, 186, 1), + "B7": Color.fromRGBO(13, 179, 186, 1), + "B8": Color.fromRGBO(13, 204, 186, 1), + "B9": Color.fromRGBO(13, 230, 186, 1), + "C0": Color.fromRGBO(26, 0, 186, 1), + "C1": Color.fromRGBO(26, 26, 186, 1), + "C2": Color.fromRGBO(26, 51, 186, 1), + "C3": Color.fromRGBO(26, 77, 186, 1), + "C4": Color.fromRGBO(26, 102, 186, 1), + "C5": Color.fromRGBO(26, 128, 186, 1), + "C6": Color.fromRGBO(26, 153, 186, 1), + "C7": Color.fromRGBO(26, 179, 186, 1), + "C8": Color.fromRGBO(26, 204, 186, 1), + "C9": Color.fromRGBO(26, 230, 186, 1), + "D0": Color.fromRGBO(38, 0, 186, 1), + "D1": Color.fromRGBO(38, 26, 186, 1), + "D2": Color.fromRGBO(38, 51, 186, 1), + "D3": Color.fromRGBO(38, 77, 186, 1), + "D4": Color.fromRGBO(38, 102, 186, 1), + "D5": Color.fromRGBO(38, 128, 186, 1), + "D6": Color.fromRGBO(38, 153, 186, 1), + "D7": Color.fromRGBO(38, 179, 186, 1), + "D8": Color.fromRGBO(38, 204, 186, 1), + "D9": Color.fromRGBO(38, 230, 186, 1), + "E0": Color.fromRGBO(51, 0, 186, 1), + "E1": Color.fromRGBO(51, 26, 186, 1), + "E2": Color.fromRGBO(51, 51, 186, 1), + "E3": Color.fromRGBO(51, 77, 186, 1), + "E4": Color.fromRGBO(51, 102, 186, 1), + "E5": Color.fromRGBO(51, 128, 186, 1), + "E6": Color.fromRGBO(51, 153, 186, 1), + "E7": Color.fromRGBO(51, 179, 186, 1), + "E8": Color.fromRGBO(51, 204, 186, 1), + "E9": Color.fromRGBO(51, 230, 186, 1), + "F0": Color.fromRGBO(64, 0, 186, 1), + "F1": Color.fromRGBO(64, 26, 186, 1), + "F2": Color.fromRGBO(64, 51, 186, 1), + "F3": Color.fromRGBO(64, 77, 186, 1), + "F4": Color.fromRGBO(64, 102, 186, 1), + "F5": Color.fromRGBO(64, 128, 186, 1), + "F6": Color.fromRGBO(64, 153, 186, 1), + "F7": Color.fromRGBO(64, 179, 186, 1), + "F8": Color.fromRGBO(64, 204, 186, 1), + "F9": Color.fromRGBO(64, 230, 186, 1), + "G0": Color.fromRGBO(77, 0, 186, 1), + "G1": Color.fromRGBO(77, 26, 186, 1), + "G2": Color.fromRGBO(77, 51, 186, 1), + "G3": Color.fromRGBO(77, 77, 186, 1), + "G4": Color.fromRGBO(77, 102, 186, 1), + "G5": Color.fromRGBO(77, 128, 186, 1), + "G6": Color.fromRGBO(77, 153, 186, 1), + "G7": Color.fromRGBO(77, 179, 186, 1), + "G8": Color.fromRGBO(77, 204, 186, 1), + "G9": Color.fromRGBO(77, 230, 186, 1), + "H0": Color.fromRGBO(89, 0, 186, 1), + "H1": Color.fromRGBO(89, 26, 186, 1), + "H2": Color.fromRGBO(89, 51, 186, 1), + "H3": Color.fromRGBO(89, 77, 186, 1), + "H4": Color.fromRGBO(89, 102, 186, 1), + "H5": Color.fromRGBO(89, 128, 186, 1), + "H6": Color.fromRGBO(89, 153, 186, 1), + "H7": Color.fromRGBO(89, 179, 186, 1), + "H8": Color.fromRGBO(89, 204, 186, 1), + "H9": Color.fromRGBO(89, 230, 186, 1), + "I0": Color.fromRGBO(102, 0, 186, 1), + "I1": Color.fromRGBO(102, 26, 186, 1), + "I2": Color.fromRGBO(102, 51, 186, 1), + "I3": Color.fromRGBO(102, 77, 186, 1), + "I4": Color.fromRGBO(102, 102, 186, 1), + "I5": Color.fromRGBO(102, 128, 186, 1), + "I6": Color.fromRGBO(102, 153, 186, 1), + "I7": Color.fromRGBO(102, 179, 186, 1), + "I8": Color.fromRGBO(102, 204, 186, 1), + "I9": Color.fromRGBO(102, 230, 186, 1), + "J0": Color.fromRGBO(115, 0, 186, 1), + "J1": Color.fromRGBO(115, 26, 186, 1), + "J2": Color.fromRGBO(115, 51, 186, 1), + "J3": Color.fromRGBO(115, 77, 186, 1), + "J4": Color.fromRGBO(115, 102, 186, 1), + "J5": Color.fromRGBO(115, 128, 186, 1), + "J6": Color.fromRGBO(115, 153, 186, 1), + "J7": Color.fromRGBO(115, 179, 186, 1), + "J8": Color.fromRGBO(115, 204, 186, 1), + "J9": Color.fromRGBO(115, 230, 186, 1), + "K0": Color.fromRGBO(128, 0, 186, 1), + "K1": Color.fromRGBO(128, 26, 186, 1), + "K2": Color.fromRGBO(128, 51, 186, 1), + "K3": Color.fromRGBO(128, 77, 186, 1), + "K4": Color.fromRGBO(128, 102, 186, 1), + "K5": Color.fromRGBO(128, 128, 186, 1), + "K6": Color.fromRGBO(128, 153, 186, 1), + "K7": Color.fromRGBO(128, 179, 186, 1), + "K8": Color.fromRGBO(128, 204, 186, 1), + "K9": Color.fromRGBO(128, 230, 186, 1), + "L0": Color.fromRGBO(140, 0, 186, 1), + "L1": Color.fromRGBO(140, 26, 186, 1), + "L2": Color.fromRGBO(140, 51, 186, 1), + "L3": Color.fromRGBO(140, 77, 186, 1), + "L4": Color.fromRGBO(140, 102, 186, 1), + "L5": Color.fromRGBO(140, 128, 186, 1), + "L6": Color.fromRGBO(140, 153, 186, 1), + "L7": Color.fromRGBO(140, 179, 186, 1), + "L8": Color.fromRGBO(140, 204, 186, 1), + "L9": Color.fromRGBO(140, 230, 186, 1), + "M0": Color.fromRGBO(153, 0, 186, 1), + "M1": Color.fromRGBO(153, 26, 186, 1), + "M2": Color.fromRGBO(153, 51, 186, 1), + "M3": Color.fromRGBO(153, 77, 186, 1), + "M4": Color.fromRGBO(153, 102, 186, 1), + "M5": Color.fromRGBO(153, 128, 186, 1), + "M6": Color.fromRGBO(153, 153, 186, 1), + "M7": Color.fromRGBO(153, 179, 186, 1), + "M8": Color.fromRGBO(153, 204, 186, 1), + "M9": Color.fromRGBO(153, 230, 186, 1), + "N0": Color.fromRGBO(166, 0, 186, 1), + "N1": Color.fromRGBO(166, 26, 186, 1), + "N2": Color.fromRGBO(166, 51, 186, 1), + "N3": Color.fromRGBO(166, 77, 186, 1), + "N4": Color.fromRGBO(166, 102, 186, 1), + "N5": Color.fromRGBO(166, 128, 186, 1), + "N6": Color.fromRGBO(166, 153, 186, 1), + "N7": Color.fromRGBO(166, 179, 186, 1), + "N8": Color.fromRGBO(166, 204, 186, 1), + "N9": Color.fromRGBO(166, 230, 186, 1), + "O0": Color.fromRGBO(179, 0, 186, 1), + "O1": Color.fromRGBO(179, 26, 186, 1), + "O2": Color.fromRGBO(179, 51, 186, 1), + "O3": Color.fromRGBO(179, 77, 186, 1), + "O4": Color.fromRGBO(179, 102, 186, 1), + "O5": Color.fromRGBO(179, 128, 186, 1), + "O6": Color.fromRGBO(179, 153, 186, 1), + "O7": Color.fromRGBO(179, 179, 186, 1), + "O8": Color.fromRGBO(179, 204, 186, 1), + "O9": Color.fromRGBO(179, 230, 186, 1), + "P0": Color.fromRGBO(191, 0, 186, 1), + "P1": Color.fromRGBO(191, 26, 186, 1), + "P2": Color.fromRGBO(191, 51, 186, 1), + "P3": Color.fromRGBO(191, 77, 186, 1), + "P4": Color.fromRGBO(191, 102, 186, 1), + "P5": Color.fromRGBO(191, 128, 186, 1), + "P6": Color.fromRGBO(191, 153, 186, 1), + "P7": Color.fromRGBO(191, 179, 186, 1), + "P8": Color.fromRGBO(191, 204, 186, 1), + "P9": Color.fromRGBO(191, 230, 186, 1), + "Q0": Color.fromRGBO(204, 0, 186, 1), + "Q1": Color.fromRGBO(204, 26, 186, 1), + "Q2": Color.fromRGBO(204, 51, 186, 1), + "Q3": Color.fromRGBO(204, 77, 186, 1), + "Q4": Color.fromRGBO(204, 102, 186, 1), + "Q5": Color.fromRGBO(204, 128, 186, 1), + "Q6": Color.fromRGBO(204, 153, 186, 1), + "Q7": Color.fromRGBO(204, 179, 186, 1), + "Q8": Color.fromRGBO(204, 204, 186, 1), + "Q9": Color.fromRGBO(204, 230, 186, 1), + "R0": Color.fromRGBO(217, 0, 186, 1), + "R1": Color.fromRGBO(217, 26, 186, 1), + "R2": Color.fromRGBO(217, 51, 186, 1), + "R3": Color.fromRGBO(217, 77, 186, 1), + "R4": Color.fromRGBO(217, 102, 186, 1), + "R5": Color.fromRGBO(217, 128, 186, 1), + "R6": Color.fromRGBO(217, 153, 186, 1), + "R7": Color.fromRGBO(217, 179, 186, 1), + "R8": Color.fromRGBO(217, 204, 186, 1), + "R9": Color.fromRGBO(217, 230, 186, 1), + "S0": Color.fromRGBO(230, 0, 186, 1), + "S1": Color.fromRGBO(230, 26, 186, 1), + "S2": Color.fromRGBO(230, 51, 186, 1), + "S3": Color.fromRGBO(230, 77, 186, 1), + "S4": Color.fromRGBO(230, 102, 186, 1), + "S5": Color.fromRGBO(230, 128, 186, 1), + "S6": Color.fromRGBO(230, 153, 186, 1), + "S7": Color.fromRGBO(230, 179, 186, 1), + "S8": Color.fromRGBO(230, 204, 186, 1), + "S9": Color.fromRGBO(230, 230, 186, 1), + "T0": Color.fromRGBO(242, 0, 186, 1), + "T1": Color.fromRGBO(242, 26, 186, 1), + "T2": Color.fromRGBO(242, 51, 186, 1), + "T3": Color.fromRGBO(242, 77, 186, 1), + "T4": Color.fromRGBO(242, 102, 186, 1), + "T5": Color.fromRGBO(242, 128, 186, 1), + "T6": Color.fromRGBO(242, 153, 186, 1), + "T7": Color.fromRGBO(242, 179, 186, 1), + "T8": Color.fromRGBO(242, 204, 186, 1), + "T9": Color.fromRGBO(242, 230, 186, 1) +}; diff --git a/lib/main.dart b/lib/main.dart index 22dabc9..4249243 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,10 @@ 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( @@ -20,10 +24,304 @@ void main() { 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 + 1) * 100, + height: 1100, + 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: 100, + height: 100, + 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: 100, + height: 100, + 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; @@ -50,7 +348,7 @@ class _AuthPageState extends State { const serverUrl = kDebugMode ? "http://localhost:9800" : "https://huacu.nuark.xyz"; - socket = io.io("https://huacu.nuark.xyz", { + socket = io.io(serverUrl, { "transports": ["websocket"], }); } @@ -75,7 +373,18 @@ class _AuthPageState extends State { crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ - Text("HUACU", style: Theme.of(context).textTheme.headline1), + 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, @@ -173,8 +482,9 @@ class Game { final String id; final String player1; final String player2; + final Set colors; - Game(this.id, this.player1, this.player2); + Game(this.id, this.player1, this.player2, this.colors); } class HomePage extends StatefulWidget { @@ -236,7 +546,12 @@ class _HomePageState extends State { socket.off("requestResponseResult"); Get.put(authData); Get.put(socket); - Get.put(Game(data[1]["id"], data[1]["player1"], data[1]["player2"])); + 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]); @@ -275,7 +590,15 @@ class _HomePageState extends State { return Scaffold( appBar: AppBar( - title: const Text("HUACU"), + title: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text("HUACU"), + SocketConnectionIndicator(socket: socket, size: 8), + ], + ), ), body: useMobileLayout ? Column( @@ -304,30 +627,32 @@ class _HomePageState extends State { (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( - "${data[index]} ${authData.login == data[index] ? "(you)" : ""}" - .trim(), + "$username ${you ? "(you)" : ""}".trim(), ), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ - if (incommingRequests.contains(data[index])) + if (hasRequest) InkResponse( onTap: () => - _handleRequestResponseAction(data[index], true), + _handleRequestResponseAction(username, true), child: const Icon(Icons.check), ), - if (incommingRequests.contains(data[index])) + if (hasRequest) InkResponse( - onTap: () => _handleRequestResponseAction( - data[index], false), + onTap: () => + _handleRequestResponseAction(username, false), child: const Icon(Icons.close), ), - if (authData.login != data[index]) + if (!you && !hasRequest) InkResponse( - onTap: () => _handleSendRequestClick(data[index]), + onTap: () => _handleSendRequestClick(username), child: const Icon(Icons.connect_without_contact), ), ], @@ -433,9 +758,13 @@ class _GamePageState extends State { 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() { @@ -446,47 +775,387 @@ class _GamePageState extends State { 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"])); - } else { - int statusCode = data[1]; - Get.put(socket); - Get.put(authData); - switch (statusCode) { - case 404: - Get.snackbar("Error", "Other player not found"); - Get.offAllNamed("/home"); - break; - case 410: - Get.snackbar("Error", "Other player left game"); - Get.offAllNamed("/home"); - break; - } } }); + + 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 Scaffold( - appBar: AppBar( - title: const Text("HUACU"), - ), - body: Center( - child: _buildGame(chat), + 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, + decoration: const BoxDecoration( + color: Colors.black, + borderRadius: + BorderRadius.only(bottomRight: Radius.circular(25)), + ), + child: Obx(() => Text("${10 - guessedColors.length}/10")), + ), + ), + ], + ), ), ); } - Widget _buildGame(RxList chat) { + Widget _generateGameField() { + const alphabet = "ABCDEFGHIJKLMNOPQRST"; + return Container( + width: (alphabet.length + 1) * 100, + height: 1100, + 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: 100, + height: 100, + 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: 100, + height: 100, + alignment: Alignment.center, + child: Text("$j", style: const TextStyle(fontSize: 40)), + ), + ), + ], + ), + ); + } + + Widget _generateStaticField() { + const alphabet = "ABCDEFGHIJKLMNOPQRST"; + return Container( + width: (alphabet.length + 1) * 100, + height: 1100, + 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: 100, + height: 100, + 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: 100, + height: 100, + 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( diff --git a/lib/socket_connection_indicator.dart b/lib/socket_connection_indicator.dart new file mode 100644 index 0000000..ff217ad --- /dev/null +++ b/lib/socket_connection_indicator.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:socket_io_client/socket_io_client.dart'; + +class SocketConnectionIndicator extends StatefulWidget { + final Socket socket; + final double size; + + const SocketConnectionIndicator( + {super.key, required this.socket, this.size = 12}); + + @override + State createState() => + _SocketConnectionIndicatorState(); +} + +class _SocketConnectionIndicatorState extends State { + var connectionStateColor = Colors.amber.obs; + + @override + void initState() { + super.initState(); + + connectionStateColor = + widget.socket.connected ? Colors.green.obs : Colors.red.obs; + + widget.socket.onConnect((data) { + connectionStateColor.value = Colors.green; + }); + widget.socket.onConnecting((data) { + connectionStateColor.value = Colors.amber; + }); + widget.socket.onReconnectAttempt((data) { + connectionStateColor.value = Colors.blue; + }); + widget.socket.onDisconnect((data) { + connectionStateColor.value = Colors.red; + }); + } + + @override + Widget build(BuildContext context) { + return InkResponse( + onTap: () { + widget.socket.disconnect().connect(); + }, + child: Obx( + () => Container( + width: widget.size, + height: widget.size, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: connectionStateColor.value, + ), + ), + ), + ); + } +} diff --git a/lib/static_color_plate.dart b/lib/static_color_plate.dart new file mode 100644 index 0000000..b302cc9 --- /dev/null +++ b/lib/static_color_plate.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; + +import 'game_pallete.dart'; + +class StaticColorPlate extends StatelessWidget { + final String plateName; + final double size; + final bool marked; + + const StaticColorPlate({ + super.key, + required this.plateName, + required this.size, + required this.marked, + }); + + @override + Widget build(BuildContext context) { + return Container( + width: size, + height: size, + decoration: BoxDecoration( + color: pallete[plateName], + border: Border.all( + color: marked ? Colors.greenAccent : Colors.black, + width: 4, + ), + borderRadius: BorderRadius.circular(10), + ), + ); + } +}