diff --git a/lib/game_pallete.dart b/lib/game_pallete.dart index d0f09dd..772ce30 100644 --- a/lib/game_pallete.dart +++ b/lib/game_pallete.dart @@ -202,3 +202,11 @@ const pallete = { "T8": Color.fromRGBO(242, 204, 186, 1), "T9": Color.fromRGBO(242, 230, 186, 1) }; + +final avatarColors = [ + Color(0xffA3A948), + Color(0xffEDB92E), + Color(0xffF85931), + Color(0xffCE1836), + Color(0xff009989) +]; diff --git a/lib/models/auth_data.dart b/lib/models/auth_data.dart index ae96cdb..bfd236f 100644 --- a/lib/models/auth_data.dart +++ b/lib/models/auth_data.dart @@ -1,6 +1,7 @@ class AuthData { + final String id; final String login; final String password; - const AuthData(this.login, this.password); + const AuthData(this.id, this.login, this.password); } diff --git a/lib/models/settings_item_model.dart b/lib/models/settings_item_model.dart new file mode 100644 index 0000000..8b6d6db --- /dev/null +++ b/lib/models/settings_item_model.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +class SettingsItemModel { + final IconData icon; + final Color color; + final String title; + final String description; + final VoidCallback? onTap; + + const SettingsItemModel({ + required this.color, + required this.description, + required this.icon, + required this.title, + this.onTap, + }); +} diff --git a/lib/models/user_data.dart b/lib/models/user_data.dart new file mode 100644 index 0000000..00ba629 --- /dev/null +++ b/lib/models/user_data.dart @@ -0,0 +1,6 @@ +class UserData { + final int gamesWon; + final int gamesLost; + + const UserData(this.gamesWon, this.gamesLost); +} diff --git a/lib/ui/pages/auth_page.dart b/lib/ui/pages/auth_page.dart index bf5674c..f4fba09 100644 --- a/lib/ui/pages/auth_page.dart +++ b/lib/ui/pages/auth_page.dart @@ -113,7 +113,9 @@ class _AuthPageState extends State { message, ); if (ok) { - Get.put(AuthData(loginController.text, passwordController.text)); + Get.put( + AuthData(data[2], loginController.text, passwordController.text), + ); Get.put(socket); Get.offNamed("/home"); } diff --git a/lib/ui/pages/home_page.dart b/lib/ui/pages/home_page.dart index 6d15251..21b41c1 100644 --- a/lib/ui/pages/home_page.dart +++ b/lib/ui/pages/home_page.dart @@ -1,10 +1,17 @@ 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}); @@ -16,6 +23,7 @@ class HomePage extends StatefulWidget { class _HomePageState extends State { final io.Socket socket = Get.find(); final AuthData authData = Get.find(); + UserData userData = const UserData(0, 0); final availableGames = [].obs; @@ -52,6 +60,19 @@ class _HomePageState extends State { 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) { @@ -83,7 +104,36 @@ class _HomePageState extends State { return Scaffold( appBar: AppBar( - title: Obx(() => Text("Available ${availableGames.length} game(s)")), + 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, @@ -133,26 +183,29 @@ class _HomePageState extends State { 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, + 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, + ), ), - ), - ], + ], + ), ), ], ), @@ -250,8 +303,8 @@ class _HomePageState extends State { content: const Text("Are you sure you want to delete this game?"), textConfirm: "Delete", onConfirm: () { - socket.emit("removeGame", gameId); Get.back(); + socket.emit("removeGame", gameId); }, textCancel: "Cancel", onCancel: () { @@ -259,4 +312,89 @@ class _HomePageState extends State { }, ); } + + 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); + } } diff --git a/lib/ui/pages/testing_grounds_page.dart b/lib/ui/pages/testing_grounds_page.dart index 89e1930..3f8fd04 100644 --- a/lib/ui/pages/testing_grounds_page.dart +++ b/lib/ui/pages/testing_grounds_page.dart @@ -12,7 +12,7 @@ class TestingGroundsPage extends StatefulWidget { class _TestingGroundsPageState extends State { final availableGames = [].obs; - final AuthData authData = const AuthData("nuark", "123"); //Get.find(); + final AuthData authData = const AuthData("123", "nuark", "123"); @override void initState() { diff --git a/lib/ui/widgets/settings.dart b/lib/ui/widgets/settings.dart new file mode 100644 index 0000000..275fe47 --- /dev/null +++ b/lib/ui/widgets/settings.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:huacu_mobile/models/settings_item_model.dart'; +import 'package:huacu_mobile/ui/widgets/settings_item.dart'; +import 'package:styled_widget/styled_widget.dart'; + +class Settings extends StatelessWidget { + final List settingsItems; + + const Settings({super.key, required this.settingsItems}); + + @override + Widget build(BuildContext context) => settingsItems + .map((settingsItem) => SettingsItem( + icon: settingsItem.icon, + iconBgColor: settingsItem.color, + title: settingsItem.title, + description: settingsItem.description, + onTap: settingsItem.onTap, + )) + .toList() + .toColumn(); +} diff --git a/lib/ui/widgets/settings_item.dart b/lib/ui/widgets/settings_item.dart new file mode 100644 index 0000000..dd6a0b3 --- /dev/null +++ b/lib/ui/widgets/settings_item.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:styled_widget/styled_widget.dart'; + +class SettingsItem extends StatefulWidget { + final IconData icon; + final Color iconBgColor; + final String title; + final String description; + final VoidCallback? onTap; + + const SettingsItem({ + super.key, + required this.icon, + required this.iconBgColor, + required this.title, + required this.description, + this.onTap, + }); + + @override + State createState() => _SettingsItemState(); +} + +class _SettingsItemState extends State { + bool pressed = false; + + @override + Widget build(BuildContext context) { + final Widget icon = Icon(widget.icon, size: 20, color: Colors.white) + .padding(all: 12) + .decorated( + color: widget.iconBgColor, + borderRadius: BorderRadius.circular(30), + ) + .padding(left: 15, right: 10); + + final Widget title = Text( + widget.title, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ).padding(bottom: 5); + + final Widget description = Text( + widget.description, + style: const TextStyle( + color: Colors.white60, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ); + + return settingsItem( + child: [ + icon, + [ + title, + description, + ].toColumn( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + ), + ].toRow(), + ); + } + + Widget settingsItem({required Widget child}) => Styled.widget(child: child) + .alignment(Alignment.center) + .borderRadius(all: 15) + .ripple() + .backgroundColor(Colors.grey.shade900, animate: true) + .clipRRect(all: 25) // clip ripple + .borderRadius(all: 25, animate: true) + .elevation( + pressed ? 0 : 20, + borderRadius: BorderRadius.circular(25), + shadowColor: const Color(0x30000000), + ) // shadow borderRadius + .constrained(height: 80) + .padding(vertical: 12) // margin + .gestures( + onTapChange: (tapStatus) => setState(() => pressed = tapStatus), + onTap: widget.onTap, + ) + .scale(all: pressed ? 0.95 : 1.0, animate: true) + .animate(const Duration(milliseconds: 150), Curves.easeOut); +} diff --git a/lib/ui/widgets/user_card.dart b/lib/ui/widgets/user_card.dart new file mode 100644 index 0000000..7a798fd --- /dev/null +++ b/lib/ui/widgets/user_card.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_boring_avatars/flutter_boring_avatars.dart'; +import 'package:huacu_mobile/game_pallete.dart'; +import 'package:huacu_mobile/models/auth_data.dart'; +import 'package:huacu_mobile/models/user_data.dart'; +import 'package:styled_widget/styled_widget.dart'; + +class UserCard extends StatelessWidget { + final AuthData authData; + final UserData userData; + + const UserCard({Key? key, required this.authData, required this.userData}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return [_buildUserRow(), _buildUserStats()] + .toColumn(mainAxisAlignment: MainAxisAlignment.spaceAround) + .padding(horizontal: 20) + .decorated( + color: Colors.blueAccent.shade400, + borderRadius: BorderRadius.circular(20)) + .elevation( + 5, + shadowColor: Colors.blueAccent.shade400, + borderRadius: BorderRadius.circular(20), + ) + .height(175) + .alignment(Alignment.topCenter); + } + + Widget _buildUserRow() { + return [ + BoringAvatars( + name: authData.id, + colors: avatarColors, + type: BoringAvatarsType.beam, + ).constrained(height: 50, width: 50).padding(right: 10), + [ + Text( + authData.login, + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ).padding(bottom: 5), + Text( + "Our one of the best player", + style: TextStyle( + color: Colors.white.withOpacity(0.6), + fontSize: 12, + ), + ), + ].toColumn(crossAxisAlignment: CrossAxisAlignment.start), + ].toRow(); + } + + Widget _buildUserStatsItem(String value, String text) => [ + Text(value).fontSize(20).textColor(Colors.white).padding(bottom: 5), + Text(text).textColor(Colors.white.withOpacity(0.6)).fontSize(12), + ].toColumn(); + + Widget _buildUserStats() { + return [ + _buildUserStatsItem(userData.gamesWon.toString(), 'Wins'), + _buildUserStatsItem(userData.gamesLost.toString(), 'Losses'), + ] + .toRow(mainAxisAlignment: MainAxisAlignment.spaceAround) + .padding(vertical: 10); + } +} diff --git a/pubspec.lock b/pubspec.lock index f97dcc1..909ff86 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -62,6 +62,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_boring_avatars: + dependency: "direct main" + description: + name: flutter_boring_avatars + sha256: "6b4a5e913b7b580ef372761c4134167a832c4054b1f409ddd6812b477d4183d7" + url: "https://pub.dev" + source: hosted + version: "1.0.1" flutter_lints: dependency: "direct dev" description: @@ -208,6 +216,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.1" + svg_path_parser: + dependency: transitive + description: + name: svg_path_parser + sha256: c0864f85efd1024e9f667ddcda10601b1199e3980fb07d63701f9b9a047bdab3 + url: "https://pub.dev" + source: hosted + version: "1.1.1" term_glyph: dependency: transitive description: @@ -234,3 +250,4 @@ packages: version: "2.1.4" sdks: dart: ">=2.19.0 <4.0.0" + flutter: ">=1.17.0" diff --git a/pubspec.yaml b/pubspec.yaml index ea3e3d9..7805386 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,6 +38,7 @@ dependencies: socket_io_client: ^2.0.1 get: ^4.6.5 styled_widget: ^0.4.1 + flutter_boring_avatars: ^1.0.1 dev_dependencies: flutter_test: