import 'dart:developer'; 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()), ], )); } 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 { final wsUrl = Uri.parse("ws://localhost:9800/ws"); late io.Socket socket; TextEditingController loginController = TextEditingController(); TextEditingController passwordController = TextEditingController(); @override void initState() { super.initState(); socket = io.io("http://localhost:9800/", { "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 HomePage extends StatefulWidget { const HomePage({super.key}); @override State createState() => _HomePageState(); } class _HomePageState extends State { late io.Socket socket; late AuthData authData = Get.find(); var guessersPlayers = [].obs; var suggestersPlayers = [].obs; var incommingRequests = [].obs; @override void initState() { super.initState(); socket = Get.find(); 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.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 bool) {} }