import 'package:cabo_counter/core/constants.dart'; import 'package:cabo_counter/core/custom_theme.dart'; import 'package:cabo_counter/data/dto/game_manager.dart'; import 'package:cabo_counter/data/dto/game_session.dart'; import 'package:cabo_counter/l10n/generated/app_localizations.dart'; import 'package:cabo_counter/presentation/views/home/active_game/active_game_view.dart'; import 'package:cabo_counter/presentation/views/home/active_game/mode_selection_view.dart'; import 'package:cabo_counter/presentation/widgets/custom_button.dart'; import 'package:cabo_counter/services/config_service.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; import 'package:uuid/uuid.dart'; enum CreateStatus { noGameTitle, noModeSelected, minPlayers, maxPlayers, noPlayerName, } /// A view for creating a new game session in the Cabo Counter app. /// /// The [CreateGameView] allows users to input a game title, select a game mode, /// add and reorder player names, and validate all required fields before /// starting a new game. It provides feedback dialogs for missing or invalid /// input and navigates to the active game view upon successful creation. class CreateGameView extends StatefulWidget { final GameMode gameMode; final String? gameTitle; final List? players; const CreateGameView({ super.key, this.gameTitle, this.players, required this.gameMode, }); @override // ignore: library_private_types_in_public_api _CreateGameViewState createState() => _CreateGameViewState(); } class _CreateGameViewState extends State { final TextEditingController _gameTitleTextController = TextEditingController(); /// List of text controllers for player names. final List _playerNameTextControllers = [ TextEditingController() ]; /// List of focus nodes for player name text fields. final List _playerNameFocusNodes = [FocusNode()]; /// Maximum number of players allowed in the game. final int maxPlayers = 5; /// Factor to adjust the view length when the keyboard is visible. final double keyboardHeightAdjustmentFactor = 0.75; /// Variable to hold the selected game mode. late GameMode gameMode; @override void initState() { super.initState(); gameMode = widget.gameMode; _gameTitleTextController.text = widget.gameTitle ?? ''; if (widget.players != null) { _playerNameTextControllers.clear(); for (var player in widget.players!) { _playerNameTextControllers.add(TextEditingController(text: player)); _playerNameFocusNodes.add(FocusNode()); } } } @override Widget build(BuildContext context) { return PopScope( canPop: false, onPopInvokedWithResult: (bool didPop, dynamic result) async { if (!didPop) { await _keyboardDelay(); if (context.mounted) Navigator.pop(context); } }, child: CupertinoPageScaffold( resizeToAvoidBottomInset: false, navigationBar: CupertinoNavigationBar( previousPageTitle: AppLocalizations.of(context).games, middle: Text(AppLocalizations.of(context).new_game), ), child: SafeArea( child: SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.fromLTRB(10, 10, 0, 0), child: Text( AppLocalizations.of(context).game, style: CustomTheme.rowTitle, ), ), Padding( padding: const EdgeInsets.fromLTRB(15, 10, 10, 0), child: CupertinoTextField( decoration: const BoxDecoration(), maxLength: 20, prefix: Text(AppLocalizations.of(context).name), textAlign: TextAlign.right, placeholder: AppLocalizations.of(context).game_title, controller: _gameTitleTextController, onSubmitted: (_) { _playerNameFocusNodes.isNotEmpty ? _playerNameFocusNodes[0].requestFocus() : FocusScope.of(context).unfocus(); }, textInputAction: _playerNameFocusNodes.isNotEmpty ? TextInputAction.next : TextInputAction.done, ), ), Padding( padding: const EdgeInsets.fromLTRB(15, 10, 10, 0), child: CupertinoTextField( decoration: const BoxDecoration(), readOnly: true, prefix: Text(AppLocalizations.of(context).mode), suffix: Row( children: [ _getDisplayedGameMode(), const SizedBox(width: 3), const CupertinoListTileChevron(), ], ), onTap: () async { await _keyboardDelay(); if (context.mounted) { final selectedMode = await Navigator.push( context, CupertinoPageRoute( builder: (context) => ModeSelectionMenu( pointLimit: ConfigService.getPointLimit(), showDeselection: false, ), ), ); setState(() { gameMode = selectedMode ?? gameMode; }); } }, ), ), Padding( padding: const EdgeInsets.fromLTRB(10, 10, 0, 0), child: Text( AppLocalizations.of(context).players, style: CustomTheme.rowTitle, ), ), ReorderableListView.builder( shrinkWrap: true, physics: const BouncingScrollPhysics(), padding: const EdgeInsets.all(8), itemCount: _playerNameTextControllers.length, onReorder: (oldIndex, newIndex) { setState(() { if (oldIndex < _playerNameTextControllers.length && newIndex <= _playerNameTextControllers.length) { if (newIndex > oldIndex) newIndex--; final item = _playerNameTextControllers.removeAt(oldIndex); _playerNameTextControllers.insert(newIndex, item); } }); }, itemBuilder: (context, index) { return Padding( key: ValueKey(index), padding: const EdgeInsets.symmetric(vertical: 8.0), child: Row( children: [ CupertinoButton( padding: EdgeInsets.zero, child: Icon( CupertinoIcons.minus_circle_fill, color: CustomTheme.red, size: 25, ), onPressed: () { setState(() { _playerNameTextControllers[index].dispose(); _playerNameTextControllers.removeAt(index); }); }, ), Expanded( child: CupertinoTextField( controller: _playerNameTextControllers[index], focusNode: _playerNameFocusNodes[index], maxLength: 12, placeholder: '${AppLocalizations.of(context).player} ${index + 1}', padding: const EdgeInsets.all(12), decoration: const BoxDecoration(), textInputAction: index + 1 < _playerNameTextControllers.length ? TextInputAction.next : TextInputAction.done, onSubmitted: (_) { if (index + 1 < _playerNameFocusNodes.length) { _playerNameFocusNodes[index + 1] .requestFocus(); } else { FocusScope.of(context).unfocus(); } }, ), ), AnimatedOpacity( opacity: _playerNameTextControllers.length > 1 ? 1.0 : 0.0, duration: const Duration( milliseconds: Constants.kFadeInDuration), child: Padding( padding: const EdgeInsets.only(right: 8.0), child: ReorderableDragStartListener( index: index, child: const Icon( CupertinoIcons.line_horizontal_3, color: CupertinoColors.systemGrey, ), ), ), ) ], ), ); }), Padding( padding: const EdgeInsets.fromLTRB(8, 0, 8, 50), child: Stack( children: [ Row( mainAxisAlignment: MainAxisAlignment.start, children: [ CupertinoButton( padding: EdgeInsets.zero, onPressed: null, child: Icon( CupertinoIcons.plus_circle_fill, color: CustomTheme.primaryColor, size: 25, ), ), ], ), Center( child: CupertinoButton( padding: const EdgeInsets.symmetric(horizontal: 0), child: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ Expanded( child: Center( child: Text( AppLocalizations.of(context).add_player, style: TextStyle( color: CustomTheme.primaryColor), ), ), ), ], ), onPressed: () { if (_playerNameTextControllers.length < maxPlayers) { setState(() { _playerNameTextControllers .add(TextEditingController()); _playerNameFocusNodes.add(FocusNode()); }); WidgetsBinding.instance .addPostFrameCallback((_) { _playerNameFocusNodes.last.requestFocus(); }); } else { _showFeedbackDialog(CreateStatus.maxPlayers); } }, ), ), ], ), ), Padding( padding: const EdgeInsets.fromLTRB(0, 0, 0, 50), child: Center( child: CustomButton( child: Text( AppLocalizations.of(context).create_game, style: TextStyle( color: CustomTheme.primaryColor, ), ), onPressed: () async { await _keyboardDelay(); _checkAllGameAttributes(); }, ), ), ), KeyboardVisibilityBuilder(builder: (context, visible) { if (visible) { return SizedBox( height: MediaQuery.of(context).viewInsets.bottom * keyboardHeightAdjustmentFactor, ); } else { return const SizedBox.shrink(); } }) ], ), )))); } /// Returns a widget that displays the currently selected game mode in the View. Text _getDisplayedGameMode() { if (gameMode == GameMode.none) { return Text(AppLocalizations.of(context).no_mode_selected); } else if (gameMode == GameMode.pointLimit) { return Text( '${ConfigService.getPointLimit()} ${AppLocalizations.of(context).points}', style: TextStyle(color: CustomTheme.primaryColor)); } else { return Text(AppLocalizations.of(context).unlimited, style: TextStyle(color: CustomTheme.primaryColor)); } } /// Checks all game attributes before creating a new game. /// If any attribute is invalid, it shows a feedback dialog. /// If all attributes are valid, it calls the `_createGame` method. void _checkAllGameAttributes() { if (_gameTitleTextController.text == '') { _showFeedbackDialog(CreateStatus.noGameTitle); return; } if (gameMode == GameMode.none) { _showFeedbackDialog(CreateStatus.noModeSelected); return; } if (_playerNameTextControllers.length < 2) { _showFeedbackDialog(CreateStatus.minPlayers); return; } if (!_everyPlayerHasAName()) { _showFeedbackDialog(CreateStatus.noPlayerName); return; } _createGame(); } /// Checks if every player has a name. /// Returns true if all players have a name, false otherwise. bool _everyPlayerHasAName() { for (var controller in _playerNameTextControllers) { if (controller.text == '') { return false; } } return true; } /// Displays a feedback dialog based on the [CreateStatus]. void _showFeedbackDialog(CreateStatus status) { final (title, message) = _getDialogContent(status); showCupertinoDialog( context: context, builder: (context) { return CupertinoAlertDialog( title: Text(title), content: Text(message), actions: [ CupertinoDialogAction( child: Text(AppLocalizations.of(context).ok), onPressed: () => Navigator.pop(context), ), ], ); }); } /// Returns the title and message for the dialog based on the [CreateStatus]. (String, String) _getDialogContent(CreateStatus status) { switch (status) { case CreateStatus.noGameTitle: return ( AppLocalizations.of(context).no_gameTitle_title, AppLocalizations.of(context).no_gameTitle_message ); case CreateStatus.noModeSelected: return ( AppLocalizations.of(context).no_mode_title, AppLocalizations.of(context).no_mode_message ); case CreateStatus.minPlayers: return ( AppLocalizations.of(context).min_players_title, AppLocalizations.of(context).min_players_message ); case CreateStatus.maxPlayers: return ( AppLocalizations.of(context).max_players_title, AppLocalizations.of(context).max_players_message ); case CreateStatus.noPlayerName: return ( AppLocalizations.of(context).no_name_title, AppLocalizations.of(context).no_name_message ); } } /// Creates a new gameSession and navigates to the active game view. /// This method creates a new gameSession object with the provided attributes in the text fields. /// It then adds the game session to the game manager and navigates to the active game view. void _createGame() { var uuid = const Uuid(); final String id = uuid.v1(); List players = []; for (var controller in _playerNameTextControllers) { players.add(controller.text); } bool isPointsLimitEnabled = gameMode == GameMode.pointLimit; GameSession gameSession = GameSession( id: id, createdAt: DateTime.now(), gameTitle: _gameTitleTextController.text, players: players, pointLimit: ConfigService.getPointLimit(), caboPenalty: ConfigService.getCaboPenalty(), isPointsLimitEnabled: isPointsLimitEnabled, ); gameManager.addGameSession(gameSession); final session = gameManager.getGameSessionById(id) ?? gameSession; Navigator.pushAndRemoveUntil( context, CupertinoPageRoute( builder: (context) => ActiveGameView(gameSession: session)), (Route route) => route.isFirst, ); } /// If the keyboard is visible, this method will unfocus the current text field /// to prevent the keyboard from interfering with the navigation bar. Future _keyboardDelay() async { if (!KeyboardVisibilityController().isVisible) { return; } else { FocusScope.of(context).unfocus(); await Future.delayed( const Duration(milliseconds: Constants.kKeyboardDelay)); } } @override void dispose() { _gameTitleTextController.dispose(); for (var controller in _playerNameTextControllers) { controller.dispose(); } for (var focusnode in _playerNameFocusNodes) { focusnode.dispose(); } super.dispose(); } }