import 'package:flutter/material.dart'; import 'package:flutter_boring_avatars/flutter_boring_avatars.dart'; import 'package:get/get.dart'; import 'package:huacu_mobile/game_pallete.dart'; import 'package:huacu_mobile/models/auth_data.dart'; import 'package:huacu_mobile/models/available_game.dart'; import 'package:huacu_mobile/models/game.dart'; import 'package:huacu_mobile/models/settings_item_model.dart'; import 'package:huacu_mobile/models/user_data.dart'; import 'package:huacu_mobile/ui/widgets/settings.dart'; import 'package:huacu_mobile/ui/widgets/socket_connection_indicator.dart'; import 'package:huacu_mobile/ui/widgets/user_card.dart'; import 'package:socket_io_client/socket_io_client.dart' as io; import 'package:styled_widget/styled_widget.dart'; 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(); UserData userData = const UserData(0, 0); final availableGames = [].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]; availableGames.value = (data["availableGames"] as List? ?? []) .map((e) => AvailableGame( id: e["id"], opponentName: e["player"], tries: e["tries"], neededRole: e["neededRole"], )) .toList(growable: false); } else { Get.snackbar("Error", "Update failed with message: ${update[1]}"); } }); socket.on("updateNeeded", (data) { socket.emit("getUpdate"); }); socket.on("removeGameResponse", (data) => null); socket.on("createGameResponse", (data) => null); socket.on("getUserDataResponse", (data) { bool ok = data[0]; if (ok) { userData = UserData( data[1]["client"]["wins"], data[1]["client"]["losses"], ); } else { Get.snackbar("Error", "Failed to get user data:\n ${data[1]}"); } }); socket.emit("getUserData"); socket.on("joinGameResponse", (data) { bool ok = data[0]; if (ok) { Get.put(authData); Get.put(socket); Get.put(Game( data[1]["id"], data[1]["guesser"], data[1]["suggester"], data[1]["tries"], (data[2] as List).map((e) => e.toString()).toSet(), )); Get.offNamed("/game"); } else { Get.snackbar("Request response", data[1]); } }); socket.emit("getUpdate"); } @override void dispose() { socket.off("hello"); socket.off("update"); socket.off("updateNeeded"); socket.off("removeGameResponse"); socket.off("createGameResponse"); socket.off("getUserDataResponse"); socket.off("joinGameResponse"); super.dispose(); } @override Widget build(BuildContext context) { final size = MediaQuery.of(context).size; return Scaffold( appBar: AppBar( title: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ const Text("HUACU"), SocketConnectionIndicator(socket: socket, size: 8), ], ), Obx( () => Text( "Available ${availableGames.length} game(s)", style: const TextStyle(fontSize: 12), ), ), ], ), actions: [ InkResponse( onTap: _openProfile, child: BoringAvatars( name: authData.id, colors: avatarColors, type: BoringAvatarsType.beam, ).paddingAll(8), ), ], ), body: SizedBox( width: size.width, height: size.height, child: Column( children: [ 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: [ if (!you) InkResponse( onTap: () => _joinGame(game.id), child: const Icon(Icons.connect_without_contact), ), if (you) InkResponse( onTap: () => _deleteGame(game.id), child: const Icon(Icons.delete), ), ], ), ), ); }, ), availableGames, ), ), Padding( padding: const EdgeInsets.all(16), child: 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 { var tries = 20; var role = "guesser"; final data = await Get.dialog?>( StatefulBuilder(builder: (buildContext, setState) { return AlertDialog( title: const Text("New game configuration"), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("Tries: $tries"), Slider( value: tries.toDouble(), label: tries.toString(), onChanged: (value) { setState(() { tries = value.round(); }); }, min: 10, max: 50, divisions: 40, ), const Text("Needed role:"), Row( children: [ Expanded( child: DropdownButton( value: role, items: const [ DropdownMenuItem( value: "guesser", child: Text("Guesser"), ), DropdownMenuItem( value: "suggester", child: Text("Suggester"), ), ], onChanged: (value) { setState(() { 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; tries = data["tries"]; role = data["role"]; socket.emit("createGame", [tries, role]); Get.snackbar("Game created", "Game created"); } void _joinGame(String gameId) { socket.emit("joinGame", gameId); } void _deleteGame(String gameId) { Get.defaultDialog( title: "Delete game", content: const Text("Are you sure you want to delete this game?"), textConfirm: "Delete", onConfirm: () { Get.back(); socket.emit("removeGame", gameId); }, textCancel: "Cancel", onCancel: () { Get.back(); }, ); } void _onLogoutTap() { socket.emit("logout"); } void _onAccountDeletedHandler(dynamic data) { if (data[0]) { Get.back(); Get.offAllNamed("/auth"); } else { Get.snackbar("Error", "Failed to delete account:\n${data[1]}"); } socket.off("deleteAccountResponse", _onAccountDeletedHandler); } void _onDeleteAccountTap() async { final textEditingController = TextEditingController(); bool ok = await Get.defaultDialog( title: "Delete game", content: [ const Text( "Are you sure you want to delete your account? This action is irreversible.", ), const Text("Enter your password to confirm:"), TextField( controller: textEditingController, obscureText: true, ), ].toColumn( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, ), textConfirm: "Delete", onConfirm: () { Get.back(result: true); }, textCancel: "Cancel", onCancel: () { Get.back(result: false); }, ); final password = textEditingController.text; if (!ok || password.isEmpty) return; socket.on("deleteAccountResponse", _onAccountDeletedHandler); socket.emit("deleteAccount", password); } void _openProfile() { Get.bottomSheet(BottomSheet( onClosing: () {}, clipBehavior: Clip.antiAliasWithSaveLayer, builder: (context) => _buildBottomSheetProfileContent(), )); } Widget _buildBottomSheetProfileContent() { page({required Widget child}) => Styled.widget(child: child) .padding(vertical: 30, horizontal: 20) .scrollable(); return [ const Text( 'Your account', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 32), ).alignment(Alignment.center).padding(bottom: 20), UserCard(authData: authData, userData: userData), Settings(settingsItems: [ SettingsItemModel( icon: Icons.logout, color: Colors.purpleAccent.shade200, title: "Logout", description: "We will miss you", onTap: _onLogoutTap, ), SettingsItemModel( icon: Icons.dangerous, color: Colors.redAccent, title: "Delete my account", description: "Beware - this action is irreversible", onTap: _onDeleteAccountTap, ), ]), ].toColumn(mainAxisSize: MainAxisSize.min).parent(page); } }