import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:socket_io_client/socket_io_client.dart' as io; final darkTheme = ThemeData( useMaterial3: true, primarySwatch: Colors.amber, brightness: Brightness.dark, ); void main() { runApp(GetMaterialApp( initialRoute: '/auth', title: 'HUACU', darkTheme: darkTheme, themeMode: ThemeMode.dark, defaultTransition: Transition.native, getPages: [ GetPage(name: '/auth', page: () => const AuthPage()), GetPage(name: '/home', page: () => const HomePage()), GetPage(name: '/game', page: () => const GamePage()), ], )); } 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("https://huacu.nuark.xyz", { "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: [ Text("HUACU", style: Theme.of(context).textTheme.headline1), 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; Game(this.id, this.player1, this.player2); } 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"])); 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: const Text("HUACU"), ), 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) { return Card( child: ListTile( title: Text( "${data[index]} ${authData.login == data[index] ? "(you)" : ""}" .trim(), ), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ if (incommingRequests.contains(data[index])) InkResponse( onTap: () => _handleRequestResponseAction(data[index], true), child: const Icon(Icons.check), ), if (incommingRequests.contains(data[index])) InkResponse( onTap: () => _handleRequestResponseAction( data[index], false), child: const Icon(Icons.close), ), if (authData.login != data[index]) InkResponse( onTap: () => _handleSendRequestClick(data[index]), 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(); var chat = [].obs; final _chatController = TextEditingController(); @override void initState() { super.initState(); socket.on("hello", (idky) { socket.dispose(); Get.offAllNamed("/auth"); }); 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; } } }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("HUACU"), ), body: Center( child: _buildGame(chat), ), ); } Widget _buildGame(RxList chat) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: ObxValue( (data) => ListView.builder( 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), ), ], ), ), ], ); } }