diff --git a/lib/main.dart b/lib/main.dart index 40d8cc4..382f789 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,11 +1,9 @@ -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; +import 'package:huacu_mobile/ui/pages/auth_page.dart'; +import 'package:huacu_mobile/ui/pages/game_page.dart'; +import 'package:huacu_mobile/ui/pages/home_page.dart'; +import 'package:huacu_mobile/ui/pages/testing_grounds_page.dart'; final darkTheme = ThemeData( useMaterial3: true, @@ -15,7 +13,7 @@ final darkTheme = ThemeData( void main() { runApp(GetMaterialApp( - initialRoute: '/auth', + initialRoute: '/testing', title: 'HUACU', darkTheme: darkTheme, themeMode: ThemeMode.dark, @@ -28,1262 +26,3 @@ void main() { ], )); } - -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), - ), - ], - ), - ), - ], - ); - } -} diff --git a/lib/models/auth_data.dart b/lib/models/auth_data.dart new file mode 100644 index 0000000..ae96cdb --- /dev/null +++ b/lib/models/auth_data.dart @@ -0,0 +1,6 @@ +class AuthData { + final String login; + final String password; + + const AuthData(this.login, this.password); +} diff --git a/lib/models/available_game.dart b/lib/models/available_game.dart new file mode 100644 index 0000000..8039b82 --- /dev/null +++ b/lib/models/available_game.dart @@ -0,0 +1,13 @@ +class AvailableGame { + final String id; + final String opponentName; + final int tries; + final String neededRole; + + AvailableGame({ + required this.id, + required this.opponentName, + required this.tries, + required this.neededRole, + }); +} diff --git a/lib/models/chat_entry.dart b/lib/models/chat_entry.dart new file mode 100644 index 0000000..bb4a6eb --- /dev/null +++ b/lib/models/chat_entry.dart @@ -0,0 +1,6 @@ +class ChatEntry { + final String from; + final String message; + + ChatEntry(this.from, this.message); +} diff --git a/lib/models/game.dart b/lib/models/game.dart new file mode 100644 index 0000000..386a5ba --- /dev/null +++ b/lib/models/game.dart @@ -0,0 +1,8 @@ +class Game { + final String id; + final String player1; + final String player2; + final Set colors; + + Game(this.id, this.player1, this.player2, this.colors); +} diff --git a/lib/ui/pages/auth_page.dart b/lib/ui/pages/auth_page.dart new file mode 100644 index 0000000..ad9a643 --- /dev/null +++ b/lib/ui/pages/auth_page.dart @@ -0,0 +1,155 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:huacu_mobile/models/auth_data.dart'; +import 'package:huacu_mobile/ui/widgets/socket_connection_indicator.dart'; +import 'package:socket_io_client/socket_io_client.dart' as io; + +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], + ); + } +} diff --git a/lib/ui/pages/game_page.dart b/lib/ui/pages/game_page.dart new file mode 100644 index 0000000..f4f4d1a --- /dev/null +++ b/lib/ui/pages/game_page.dart @@ -0,0 +1,528 @@ +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 + 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), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/ui/pages/home_page.dart b/lib/ui/pages/home_page.dart new file mode 100644 index 0000000..9fce0b5 --- /dev/null +++ b/lib/ui/pages/home_page.dart @@ -0,0 +1,258 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:huacu_mobile/models/auth_data.dart'; +import 'package:huacu_mobile/models/game.dart'; +import 'package:huacu_mobile/ui/widgets/socket_connection_indicator.dart'; +import 'package:socket_io_client/socket_io_client.dart' as io; + +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); + }); + } +} diff --git a/lib/ui/pages/testing_grounds_page.dart b/lib/ui/pages/testing_grounds_page.dart new file mode 100644 index 0000000..09a4473 --- /dev/null +++ b/lib/ui/pages/testing_grounds_page.dart @@ -0,0 +1,155 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:huacu_mobile/models/auth_data.dart'; +import 'package:huacu_mobile/models/available_game.dart'; + +class TestingGroundsPage extends StatefulWidget { + const TestingGroundsPage({super.key}); + + @override + State createState() => _TestingGroundsPageState(); +} + +class _TestingGroundsPageState extends State { + final availableGames = [].obs; + final AuthData authData = const AuthData("nuark", "123"); //Get.find(); + + @override + Widget build(BuildContext context) { + final size = MediaQuery.of(context).size; + + return Scaffold( + body: SizedBox( + width: size.width, + height: size.height, + child: Column( + children: [ + const Text("Available games"), + Obx(() => Text("Count: ${availableGames.length}")), + Expanded( + child: ObxValue( + (data) => ListView.builder( + itemCount: data.length, + itemBuilder: (context, index) { + final game = data[index]; + final you = authData.login == game.opponentName; + return Card( + child: ListTile( + title: Text( + "Game of ${game.opponentName} ${you ? "(you)" : ""}" + .trim(), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Tries: ${game.tries}"), + Text("Needed role: ${game.neededRole}"), + ], + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + InkResponse( + onTap: () {}, + child: const Icon(Icons.connect_without_contact), + ), + ], + ), + ), + ); + }, + ), + availableGames, + ), + ), + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: ObxValue( + (data) { + return ElevatedButton( + onPressed: data.firstWhereOrNull( + (g) => g.opponentName == authData.login) == + null + ? _createGame + : null, + child: const Text("Create"), + ); + }, + availableGames, + ), + ), + ], + ), + ], + ), + ), + ); + } + + void _createGame() async { + final data = await Get.dialog?>( + Builder(builder: (buildContext) { + var tries = 20; + var role = "guesser"; + + return AlertDialog( + title: const Text("New game configuration"), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text("Tries:"), + Slider( + value: 20, + onChanged: (value) { + tries = value.toInt(); + }, + min: 10, + max: 50, + divisions: 40, + ), + const Text("Needed role:"), + DropdownButton( + value: "guesser", + items: const [ + DropdownMenuItem( + value: "guesser", + child: Text("Guesser"), + ), + DropdownMenuItem( + value: "suggester", + child: Text("Suggester"), + ), + ], + onChanged: (value) { + role = value ?? "guesser"; + }, + ), + ], + ), + actions: [ + TextButton( + onPressed: () { + Get.back(); + }, + child: const Text("Cancel"), + ), + TextButton( + onPressed: () { + Get.back(result: { + "tries": tries, + "role": role, + }); + }, + child: const Text("Create"), + ), + ], + ); + })); + if (data == null) return; + + Get.snackbar("Game created", "Game created"); + } +} diff --git a/lib/color_plate.dart b/lib/ui/widgets/color_plate.dart similarity index 98% rename from lib/color_plate.dart rename to lib/ui/widgets/color_plate.dart index 4a13e69..c1fe311 100644 --- a/lib/color_plate.dart +++ b/lib/ui/widgets/color_plate.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; - -import 'game_pallete.dart'; +import 'package:huacu_mobile/game_pallete.dart'; enum PlateState { normal, diff --git a/lib/socket_connection_indicator.dart b/lib/ui/widgets/socket_connection_indicator.dart similarity index 100% rename from lib/socket_connection_indicator.dart rename to lib/ui/widgets/socket_connection_indicator.dart diff --git a/lib/static_color_plate.dart b/lib/ui/widgets/static_color_plate.dart similarity index 93% rename from lib/static_color_plate.dart rename to lib/ui/widgets/static_color_plate.dart index b302cc9..c674e5f 100644 --- a/lib/static_color_plate.dart +++ b/lib/ui/widgets/static_color_plate.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; - -import 'game_pallete.dart'; +import 'package:huacu_mobile/game_pallete.dart'; class StaticColorPlate extends StatelessWidget { final String plateName;