From d627f335797cdb8d739a4ba3a8bc458c669ce0c1 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 21 Jul 2025 13:29:25 +0200 Subject: [PATCH] Beta-Version 0.5.3 (#136) * Updated createGameView ListBuilder * Added ReorderableListView * Increment build no * Fixed bug with wrong medal icon * change not equal to greater than * Updated bool var * Fixed deletion error * Small translation improvements * Implemented first version of point overview * Visual improvements on table * Added details and sum row * Updated strings * Implemented new strings * Refactoring * Updated graph displayment * Moved new views to statistics section * Added seperator in main menu * Renaming * Updated sign * Updated colors & class name * Removed empty line * Updated round index * Updated types * Added new kamikaze button and bundles navigation functionality * Updated lock icon * Updated button position and design * Removed title row and changed segmendetControl Padding * Refactored logic and added comments * Updated comment * Chaned icon * Added comment * Removed print * Updated colors * Changed var name * Removed unused strings * Added gameMode * Changed creation variable * Updated mode selection * Updated strings * Changed mode order * Implemented default mode selection * Updated initState * Removed print * Removed print * Removed comments * Updated config service * Changed create game view * Changed icon * Updated strings * Updated config * Updated mode selection logic * Deleted getter * Removed not used code * Implemented reset logic for default game mode * Updated to 0.5.0 * Hotfix: Pixel Overflow * Changed the overall return type for gamemodes * Updated documentation * Fixed merge issues * Added Custom button * Updated strings * Updated buttons, implemented animatedOpacity * Keyboard still doesnt works * Fixed keyboard behaviour * Changed keyboard height * Added method getGameSessionById() * Updated gameSession class * id gets added to gameSession class at creation * Cleaned up file * Added docs and dependency * Removed toString * Implemented null safety * Added named parameter * Replaced button with custom button * Updated key * Updated addGameSessionMethod * Update README.md * Added Strings for popup * Implemented popup & confetti * Extracted code to method _playFinishAnimation() * Replaced tenary operator with Visibility Widget * Replaced tenary operator with Visibility Widget * Used variable again * Added delays in constants.dart * Removed confetti button * Updated strings * Removed print * Added dispose for confettiController * Implemented missing constant in code * Updated gameSession logic so more than one player can be winner * Updated strings * Updated winner popup * game names now can have up to 20 chars * Updated strings * Added sized box for visual enhancement * Centered the add player button and made it wider * New created player textfields get automatically focused * Added focus nodes for autofocus and navigation between textfields * Updated version number * Updated game title textfield with focus node and textaction * Added focusnodes to dispose * Update README.md * Fixed bug with no popup shown * Fixed bug with out of range error * Updated listener notification --- README.md | 45 +- lib/core/constants.dart | 9 + lib/core/custom_theme.dart | 8 +- lib/data/game_manager.dart | 14 +- lib/data/game_session.dart | 25 +- lib/l10n/arb/app_de.arb | 29 +- lib/l10n/arb/app_en.arb | 55 +- lib/l10n/generated/app_localizations.dart | 66 ++- lib/l10n/generated/app_localizations_de.dart | 43 +- lib/l10n/generated/app_localizations_en.dart | 67 ++- lib/main.dart | 2 - lib/presentation/views/active_game_view.dart | 479 +++++++++++------- lib/presentation/views/create_game_view.dart | 474 ++++++++++------- lib/presentation/views/graph_view.dart | 89 ++-- lib/presentation/views/main_menu_view.dart | 291 +++++------ .../views/mode_selection_view.dart | 33 +- lib/presentation/views/points_view.dart | 141 ++++++ lib/presentation/views/round_view.dart | 276 +++++----- lib/presentation/views/settings_view.dart | 43 +- lib/presentation/views/tab_view.dart | 3 +- lib/presentation/widgets/custom_button.dart | 19 + lib/services/config_service.dart | 85 +++- pubspec.yaml | 5 +- test/data/game_session_test.dart | 1 + 24 files changed, 1503 insertions(+), 799 deletions(-) create mode 100644 lib/presentation/views/points_view.dart create mode 100644 lib/presentation/widgets/custom_button.dart diff --git a/README.md b/README.md index 66eab9f..c1c7a24 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # CABO Counter -![Version](https://img.shields.io/badge/Version-0.4.7-orange) +![Version](https://img.shields.io/badge/Version-0.5.3-orange) ![Flutter](https://img.shields.io/badge/Flutter-3.32.1-blue?logo=flutter) ![Dart](https://img.shields.io/badge/Dart-3.8.1-blue?logo=dart) ![iOS](https://img.shields.io/badge/iOS-18.5-white?logo=apple) @@ -12,25 +12,29 @@ A mobile score tracker for the card game Cabo, helping players effortlessly mana ## 📱 Description -Cabo Counter is an intuitive Flutter-based mobile application designed to enhance your CABO card game experience. It eliminates manual scorekeeping by automatically calculating points per round. +Cabo Counter is an intuitive Flutter-based mobile application designed to enhance your CABO card game experience. It eliminates manual scorekeeping by automatically calculating points per round. ## ✨ Features -- 🆕 Create new games with customizable rules - 👥 Support for 2-5 players - ⚖️ Two game modes: - - **100 Points Mode** (Standard) - - **Infinite Mode** (Casual play) + - **Point Limit Mode**: Play until a certain point limit is reached + - **Unlimited Mode**: Play without an limit and end the round at any point - 🔢 Automatic score calculation with: + - Falsly calling Cabo + - Exact 100-point bonus (score halving) - Kamikaze rule handling - - Exact 100-point bonus (score halving) -- 📊 Round history tracking +- 📊 Round history tracking via graph and table +- 🎨 Customizable + - Change the default settings for point limits and cabo penaltys + - Choose a default game mode for every new created game +- 💿 Im- and exporting certain games or the whole app data ## 🚀 Getting Started ### Prerequisites -- Flutter 3.24.5+ -- Dart 3.5.4+ +- Flutter 3.32.1+ +- Dart 3.8.1+ ### Installation @@ -43,18 +47,22 @@ flutter run ## 🎮 Usage -1. **Start New Game** -- Choose game mode (100 Points or Infinite) +1. **Start a new game** +- Click the "+"-Button +- Choose a game title and a game mode - Add 2-5 players 2. **Gameplay** -- Track rounds with automatic scoring -- Handle special rules (Kamikaze, exact 100 points) -- View real-time standings +- Open the first round +- Choose the player who called Cabo +- Enter the points of every player +- If given: Choose a Kamikaze player +- Navigate to the next round or back to the overview +- Let the app calculate all points for you -3. **Round Management** -- Automatic winner detection -- Penalty point calculation +3. **Statistics** +- View the progress graph for the game +- Get a detailed table overview for every points made or lost - Game-over detection (100 Points mode) ## 🃏 Key Rules Overview @@ -67,7 +75,8 @@ flutter run - Exact 100 points: Score halved ### Game End -- First player ≥101 points triggers final scoring +- First player ≥100 points triggers final scoring +- In unlimited mode you can end the game via the End Game Button - Lowest total score wins ## 🤝 Contributing diff --git a/lib/core/constants.dart b/lib/core/constants.dart index e716464..e1c2f8d 100644 --- a/lib/core/constants.dart +++ b/lib/core/constants.dart @@ -19,4 +19,13 @@ class Constants { remindDays: 45, minLaunches: 15, remindLaunches: 40); + + /// Delay in milliseconds before a pop-up appears. + static const int popUpDelay = 300; + + /// Delay in milliseconds before the round view appears after the previous one is closed. + static const int roundViewDelay = 600; + + /// Duration in milliseconds for the fade-in animation of texts. + static const int fadeInDuration = 300; } diff --git a/lib/core/custom_theme.dart b/lib/core/custom_theme.dart index a00340b..bfc4f3c 100644 --- a/lib/core/custom_theme.dart +++ b/lib/core/custom_theme.dart @@ -4,7 +4,9 @@ class CustomTheme { static Color white = CupertinoColors.white; static Color primaryColor = CupertinoColors.systemGreen; static Color backgroundColor = const Color(0xFF101010); - static Color backgroundTintColor = CupertinoColors.darkBackgroundGray; + static Color mainElementBackgroundColor = CupertinoColors.darkBackgroundGray; + static Color playerTileColor = CupertinoColors.secondaryLabel; + static Color buttonBackgroundColor = const Color(0xFF202020); // Line Colors for GraphView static const Color graphColor1 = Color(0xFFF44336); @@ -13,6 +15,10 @@ class CustomTheme { static const Color graphColor4 = Color(0xFF9C27B0); static final Color graphColor5 = primaryColor; + // Colors for PointsView + static Color pointLossColor = primaryColor; + static const Color pointGainColor = Color(0xFFF44336); + static TextStyle modeTitle = TextStyle( color: primaryColor, fontSize: 20, diff --git a/lib/data/game_manager.dart b/lib/data/game_manager.dart index b3a1933..eca6880 100644 --- a/lib/data/game_manager.dart +++ b/lib/data/game_manager.dart @@ -1,5 +1,6 @@ import 'package:cabo_counter/data/game_session.dart'; import 'package:cabo_counter/services/local_storage_service.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; class GameManager extends ChangeNotifier { @@ -10,17 +11,25 @@ class GameManager extends ChangeNotifier { /// sorts the list in descending order based on the creation date, and notifies listeners of the change. /// It also saves the updated game sessions to local storage. /// Returns the index of the newly added session in the sorted list. - Future addGameSession(GameSession session) async { + int addGameSession(GameSession session) { session.addListener(() { notifyListeners(); // Propagate session changes }); gameList.add(session); gameList.sort((a, b) => b.createdAt.compareTo(a.createdAt)); notifyListeners(); - await LocalStorageService.saveGameSessions(); + LocalStorageService.saveGameSessions(); return gameList.indexOf(session); } + /// Retrieves a game session by its id. + /// Takes a String [id] as input. It searches the `gameList` for a session + /// with a matching id and returns it if found. + /// If no session is found, it returns null. + GameSession? getGameSessionById(String id) { + return gameList.firstWhereOrNull((session) => session.id == id); + } + /// Removes a game session from the list and sorts it by creation date. /// Takes a [index] as input. It then removes the session at the specified index from the `gameList`, /// sorts the list in descending order based on the creation date, and notifies listeners of the change. @@ -60,6 +69,7 @@ class GameManager extends ChangeNotifier { gameList[index].roundNumber--; gameList[index].isGameFinished = true; + gameList[index].setWinner(); notifyListeners(); LocalStorageService.saveGameSessions(); } diff --git a/lib/data/game_session.dart b/lib/data/game_session.dart index d1402e5..dc100a5 100644 --- a/lib/data/game_session.dart +++ b/lib/data/game_session.dart @@ -13,7 +13,7 @@ import 'package:uuid/uuid.dart'; /// [isGameFinished] is a boolean indicating if the game has ended yet. /// [winner] is the name of the player who won the game. class GameSession extends ChangeNotifier { - late String id; + final String id; final DateTime createdAt; final String gameTitle; final List players; @@ -27,6 +27,7 @@ class GameSession extends ChangeNotifier { List roundList = []; GameSession({ + required this.id, required this.createdAt, required this.gameTitle, required this.players, @@ -35,8 +36,6 @@ class GameSession extends ChangeNotifier { required this.isPointsLimitEnabled, }) { playerScores = List.filled(players.length, 0); - var uuid = const Uuid(); - id = uuid.v1(); } @override @@ -256,7 +255,7 @@ class GameSession extends ChangeNotifier { isGameFinished = true; print('${players[i]} hat die 100 Punkte ueberschritten, ' 'deswegen wurde das Spiel beendet'); - _setWinner(); + setWinner(); } } } @@ -299,16 +298,20 @@ class GameSession extends ChangeNotifier { /// Determines the winner of the game session. /// It iterates through the player scores and finds the player /// with the lowest score. - void _setWinner() { - int score = playerScores[0]; - String lowestPlayer = players[0]; + void setWinner() { + int minScore = playerScores.reduce((a, b) => a < b ? a : b); + List lowestPlayers = []; for (int i = 0; i < players.length; i++) { - if (playerScores[i] < score) { - score = playerScores[i]; - lowestPlayer = players[i]; + if (playerScores[i] == minScore) { + lowestPlayers.add(players[i]); } } - winner = lowestPlayer; + if (lowestPlayers.length > 1) { + winner = + '${lowestPlayers.sublist(0, lowestPlayers.length - 1).join(', ')} & ${lowestPlayers.last}'; + } else { + winner = lowestPlayers.first; + } notifyListeners(); } diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index 93215ed..cd1a8c7 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -55,9 +55,12 @@ "min_players_title": "Zu wenig Spieler:innen", "min_players_message": "Es müssen mindestens 2 Spieler:innen hinzugefügt werden", "no_name_title": "Kein Name", - "no_name_message": "Jeder Spieler muss einen Namen haben.", + "no_name_message": "Jede:r Spieler:in muss einen Namen haben.", "select_game_mode": "Spielmodus auswählen", + "no_mode_selected": "Wähle einen Spielmodus", + "no_default_mode": "Kein Modus", + "no_default_description": "Entscheide bei jedem Spiel selber, welchen Modus du spielen möchtest.", "point_limit_description": "Es wird so lange gespielt, bis ein:e Spieler:in mehr als {pointLimit} Punkte erreicht", "@point_limit_description": { "placeholders": { @@ -71,6 +74,7 @@ "results": "Ergebnisse", "who_said_cabo": "Wer hat CABO gesagt?", "kamikaze": "Kamikaze", + "who_has_kamikaze": "Wer hat Kamikaze?", "done": "Fertig", "next_round": "Nächste Runde", "bonus_points_title": "Bonus-Punkte!", @@ -92,7 +96,21 @@ } }, - + "end_of_game_title": "Spiel beendet", + "end_of_game_message": "{playerCount, plural, =1{{names} hat das Spiel mit {points} Punkten gewonnen. Glückwunsch!} other{{names} haben das Spiel mit {points} Punkten gewonnen. Glückwunsch!}}", + "@end_of_game_message": { + "placeholders": { + "playerCount": { + "type": "int" + }, + "names": { + "type": "String" + }, + "points": { + "type": "int" + } + } + }, "end_game": "Spiel beenden", "delete_game": "Spiel löschen", "new_game_same_settings": "Neues Spiel mit gleichen Einstellungen", @@ -102,14 +120,15 @@ "end_game_title": "Spiel beenden?", "end_game_message": "Möchtest du das Spiel beenden? Das Spiel wird als beendet markiert und kann nicht fortgeführt werden.", - "game_process": "Spielverlauf", + "statistics": "Statistiken", + "point_overview": "Punkteübersicht", + "scoring_history": "Spielverlauf", "empty_graph_text": "Du musst mindestens eine Runde spielen, damit der Graph des Spielverlaufes angezeigt werden kann.", "settings": "Einstellungen", "cabo_penalty": "Cabo-Strafe", - "cabo_penalty_subtitle": "... für falsches Cabo sagen", "point_limit": "Punkte-Limit", - "point_limit_subtitle": "... hier ist Schluss", + "standard_mode": "Standard-Modus", "reset_to_default": "Auf Standard zurücksetzen", "game_data": "Spieldaten", "import_data": "Spieldaten importieren", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 19695a5..7346343 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -14,13 +14,13 @@ "player": "Player", "players": "Players", "name": "Name", - "back": "Back", + "back": "Back", "home": "Home", "about": "About", "empty_text_1": "Pretty empty here...", - "empty_text_2": "Add a new round using the button in the top right corner.", + "empty_text_2": "Create a new game using the button in the top right.", "delete_game_title": "Delete game?", "delete_game_message": "Are you sure you want to delete the game \"{gameTitle}\"? This action cannot be undone.", "@delete_game_message": { @@ -46,19 +46,22 @@ "select_mode": "Select a mode", "add_player": "Add Player", "create_game": "Create Game", - "max_players_title": "Maximum reached", - "max_players_message": "A maximum of 5 players can be added.", - "no_gameTitle_title": "No Title", - "no_gameTitle_message": "You must enter a title for the game.", - "no_mode_title": "No Mode", - "no_mode_message": "You must select a game mode.", - "min_players_title": "Too few players", - "min_players_message": "At least 2 players must be added.", - "no_name_title": "No Name", + "max_players_title": "Player Limit Reached", + "max_players_message": "You can add a maximum of 5 players.", + "no_gameTitle_title": "Missing Game Title", + "no_gameTitle_message": "Please enter a title for your game.", + "no_mode_title": "Game Mode Required", + "no_mode_message": "Please select a game mode to continue", + "min_players_title": "Too Few Players", + "min_players_message": "At least 2 players are required to start the game.", + "no_name_title": "Missing Player Names", "no_name_message": "Each player must have a name.", "select_game_mode": "Select game mode", - "point_limit_description": "The game ends when a player reaches more than {pointLimit} points.", + "no_mode_selected": "No mode selected", + "no_default_mode": "No default mode", + "no_default_description": "The default mode gets reset.", + "point_limit_description": "The game ends when a player scores more than {pointLimit} points.", "@point_limit_description": { "placeholders": { "pointLimit": { @@ -66,11 +69,12 @@ } } }, - "unlimited_description": "There is no limit. The game continues until you decide to stop.", + "unlimited_description": "The game continues until you decide to stop playing", "results": "Results", - "who_said_cabo": "Who said CABO?", + "who_said_cabo": "Who called Cabo?", "kamikaze": "Kamikaze", + "who_has_kamikaze": "Who has Kamikaze?", "done": "Done", "next_round": "Next Round", "bonus_points_title": "Bonus-Points!", @@ -92,7 +96,21 @@ } }, - + "end_of_game_title": "End of Game", + "end_of_game_message": "{playerCount, plural, =1{{names} won the game with {points} points. Congratulations!} other{{names} won the game with {points} points. Congratulations to everyone!}}", + "@end_of_game_message": { + "placeholders": { + "playerCount": { + "type": "int" + }, + "names": { + "type": "String" + }, + "points": { + "type": "int" + } + } + }, "end_game": "End Game", "delete_game": "Delete Game", "new_game_same_settings": "New Game with same Settings", @@ -102,14 +120,15 @@ "end_game_title": "End the game?", "end_game_message": "Do you want to end the game? The game gets marked as finished and cannot be continued.", - "game_process": "Scoring History", + "statistics": "Statistics", + "point_overview": "Point Overview", + "scoring_history": "Scoring History", "empty_graph_text": "You must play at least one round for the game progress graph to be displayed.", "settings": "Settings", "cabo_penalty": "Cabo Penalty", - "cabo_penalty_subtitle": "... for falsely calling Cabo.", "point_limit": "Point Limit", - "point_limit_subtitle": "... the game ends here.", + "standard_mode": "Default Mode", "reset_to_default": "Reset to Default", "game_data": "Game Data", "import_data": "Import Data", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 0a902f6..5749079 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -365,7 +365,7 @@ abstract class AppLocalizations { /// No description provided for @no_name_message. /// /// In de, this message translates to: - /// **'Jeder Spieler muss einen Namen haben.'** + /// **'Jede:r Spieler:in muss einen Namen haben.'** String get no_name_message; /// No description provided for @select_game_mode. @@ -374,6 +374,24 @@ abstract class AppLocalizations { /// **'Spielmodus auswählen'** String get select_game_mode; + /// No description provided for @no_mode_selected. + /// + /// In de, this message translates to: + /// **'Wähle einen Spielmodus'** + String get no_mode_selected; + + /// No description provided for @no_default_mode. + /// + /// In de, this message translates to: + /// **'Kein Modus'** + String get no_default_mode; + + /// No description provided for @no_default_description. + /// + /// In de, this message translates to: + /// **'Entscheide bei jedem Spiel selber, welchen Modus du spielen möchtest.'** + String get no_default_description; + /// No description provided for @point_limit_description. /// /// In de, this message translates to: @@ -404,6 +422,12 @@ abstract class AppLocalizations { /// **'Kamikaze'** String get kamikaze; + /// No description provided for @who_has_kamikaze. + /// + /// In de, this message translates to: + /// **'Wer hat Kamikaze?'** + String get who_has_kamikaze; + /// No description provided for @done. /// /// In de, this message translates to: @@ -429,6 +453,18 @@ abstract class AppLocalizations { String bonus_points_message( int playerCount, String names, int pointLimit, int bonusPoints); + /// No description provided for @end_of_game_title. + /// + /// In de, this message translates to: + /// **'Spiel beendet'** + String get end_of_game_title; + + /// No description provided for @end_of_game_message. + /// + /// In de, this message translates to: + /// **'{playerCount, plural, =1{{names} hat das Spiel mit {points} Punkten gewonnen. Glückwunsch!} other{{names} haben das Spiel mit {points} Punkten gewonnen. Glückwunsch!}}'** + String end_of_game_message(int playerCount, String names, int points); + /// No description provided for @end_game. /// /// In de, this message translates to: @@ -477,11 +513,23 @@ abstract class AppLocalizations { /// **'Möchtest du das Spiel beenden? Das Spiel wird als beendet markiert und kann nicht fortgeführt werden.'** String get end_game_message; - /// No description provided for @game_process. + /// No description provided for @statistics. + /// + /// In de, this message translates to: + /// **'Statistiken'** + String get statistics; + + /// No description provided for @point_overview. + /// + /// In de, this message translates to: + /// **'Punkteübersicht'** + String get point_overview; + + /// No description provided for @scoring_history. /// /// In de, this message translates to: /// **'Spielverlauf'** - String get game_process; + String get scoring_history; /// No description provided for @empty_graph_text. /// @@ -501,23 +549,17 @@ abstract class AppLocalizations { /// **'Cabo-Strafe'** String get cabo_penalty; - /// No description provided for @cabo_penalty_subtitle. - /// - /// In de, this message translates to: - /// **'... für falsches Cabo sagen'** - String get cabo_penalty_subtitle; - /// No description provided for @point_limit. /// /// In de, this message translates to: /// **'Punkte-Limit'** String get point_limit; - /// No description provided for @point_limit_subtitle. + /// No description provided for @standard_mode. /// /// In de, this message translates to: - /// **'... hier ist Schluss'** - String get point_limit_subtitle; + /// **'Standard-Modus'** + String get standard_mode; /// No description provided for @reset_to_default. /// diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index 7a71d00..87745dd 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -149,11 +149,21 @@ class AppLocalizationsDe extends AppLocalizations { String get no_name_title => 'Kein Name'; @override - String get no_name_message => 'Jeder Spieler muss einen Namen haben.'; + String get no_name_message => 'Jede:r Spieler:in muss einen Namen haben.'; @override String get select_game_mode => 'Spielmodus auswählen'; + @override + String get no_mode_selected => 'Wähle einen Spielmodus'; + + @override + String get no_default_mode => 'Kein Modus'; + + @override + String get no_default_description => + 'Entscheide bei jedem Spiel selber, welchen Modus du spielen möchtest.'; + @override String point_limit_description(int pointLimit) { return 'Es wird so lange gespielt, bis ein:e Spieler:in mehr als $pointLimit Punkte erreicht'; @@ -172,6 +182,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get kamikaze => 'Kamikaze'; + @override + String get who_has_kamikaze => 'Wer hat Kamikaze?'; + @override String get done => 'Fertig'; @@ -195,6 +208,21 @@ class AppLocalizationsDe extends AppLocalizations { return '$_temp0'; } + @override + String get end_of_game_title => 'Spiel beendet'; + + @override + String end_of_game_message(int playerCount, String names, int points) { + String _temp0 = intl.Intl.pluralLogic( + playerCount, + locale: localeName, + other: + '$names haben das Spiel mit $points Punkten gewonnen. Glückwunsch!', + one: '$names hat das Spiel mit $points Punkten gewonnen. Glückwunsch!', + ); + return '$_temp0'; + } + @override String get end_game => 'Spiel beenden'; @@ -222,7 +250,13 @@ class AppLocalizationsDe extends AppLocalizations { 'Möchtest du das Spiel beenden? Das Spiel wird als beendet markiert und kann nicht fortgeführt werden.'; @override - String get game_process => 'Spielverlauf'; + String get statistics => 'Statistiken'; + + @override + String get point_overview => 'Punkteübersicht'; + + @override + String get scoring_history => 'Spielverlauf'; @override String get empty_graph_text => @@ -234,14 +268,11 @@ class AppLocalizationsDe extends AppLocalizations { @override String get cabo_penalty => 'Cabo-Strafe'; - @override - String get cabo_penalty_subtitle => '... für falsches Cabo sagen'; - @override String get point_limit => 'Punkte-Limit'; @override - String get point_limit_subtitle => '... hier ist Schluss'; + String get standard_mode => 'Standard-Modus'; @override String get reset_to_default => 'Auf Standard zurücksetzen'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 4d4d663..eb341af 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -61,7 +61,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get empty_text_2 => - 'Add a new round using the button in the top right corner.'; + 'Create a new game using the button in the top right.'; @override String get delete_game_title => 'Delete game?'; @@ -119,31 +119,32 @@ class AppLocalizationsEn extends AppLocalizations { String get create_game => 'Create Game'; @override - String get max_players_title => 'Maximum reached'; + String get max_players_title => 'Player Limit Reached'; @override - String get max_players_message => 'A maximum of 5 players can be added.'; + String get max_players_message => 'You can add a maximum of 5 players.'; @override - String get no_gameTitle_title => 'No Title'; + String get no_gameTitle_title => 'Missing Game Title'; @override - String get no_gameTitle_message => 'You must enter a title for the game.'; + String get no_gameTitle_message => 'Please enter a title for your game.'; @override - String get no_mode_title => 'No Mode'; + String get no_mode_title => 'Game Mode Required'; @override - String get no_mode_message => 'You must select a game mode.'; + String get no_mode_message => 'Please select a game mode to continue'; @override - String get min_players_title => 'Too few players'; + String get min_players_title => 'Too Few Players'; @override - String get min_players_message => 'At least 2 players must be added.'; + String get min_players_message => + 'At least 2 players are required to start the game.'; @override - String get no_name_title => 'No Name'; + String get no_name_title => 'Missing Player Names'; @override String get no_name_message => 'Each player must have a name.'; @@ -151,24 +152,36 @@ class AppLocalizationsEn extends AppLocalizations { @override String get select_game_mode => 'Select game mode'; + @override + String get no_mode_selected => 'No mode selected'; + + @override + String get no_default_mode => 'No default mode'; + + @override + String get no_default_description => 'The default mode gets reset.'; + @override String point_limit_description(int pointLimit) { - return 'The game ends when a player reaches more than $pointLimit points.'; + return 'The game ends when a player scores more than $pointLimit points.'; } @override String get unlimited_description => - 'There is no limit. The game continues until you decide to stop.'; + 'The game continues until you decide to stop playing'; @override String get results => 'Results'; @override - String get who_said_cabo => 'Who said CABO?'; + String get who_said_cabo => 'Who called Cabo?'; @override String get kamikaze => 'Kamikaze'; + @override + String get who_has_kamikaze => 'Who has Kamikaze?'; + @override String get done => 'Done'; @@ -192,6 +205,21 @@ class AppLocalizationsEn extends AppLocalizations { return '$_temp0'; } + @override + String get end_of_game_title => 'End of Game'; + + @override + String end_of_game_message(int playerCount, String names, int points) { + String _temp0 = intl.Intl.pluralLogic( + playerCount, + locale: localeName, + other: + '$names won the game with $points points. Congratulations to everyone!', + one: '$names won the game with $points points. Congratulations!', + ); + return '$_temp0'; + } + @override String get end_game => 'End Game'; @@ -219,7 +247,13 @@ class AppLocalizationsEn extends AppLocalizations { 'Do you want to end the game? The game gets marked as finished and cannot be continued.'; @override - String get game_process => 'Scoring History'; + String get statistics => 'Statistics'; + + @override + String get point_overview => 'Point Overview'; + + @override + String get scoring_history => 'Scoring History'; @override String get empty_graph_text => @@ -231,14 +265,11 @@ class AppLocalizationsEn extends AppLocalizations { @override String get cabo_penalty => 'Cabo Penalty'; - @override - String get cabo_penalty_subtitle => '... for falsely calling Cabo.'; - @override String get point_limit => 'Point Limit'; @override - String get point_limit_subtitle => '... the game ends here.'; + String get standard_mode => 'Default Mode'; @override String get reset_to_default => 'Reset to Default'; diff --git a/lib/main.dart b/lib/main.dart index 9279426..8b45434 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,8 +12,6 @@ Future main() async { await SystemChrome.setPreferredOrientations( [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]); await ConfigService.initConfig(); - ConfigService.pointLimit = await ConfigService.getPointLimit(); - ConfigService.caboPenalty = await ConfigService.getCaboPenalty(); await VersionService.init(); runApp(const App()); } diff --git a/lib/presentation/views/active_game_view.dart b/lib/presentation/views/active_game_view.dart index ab07804..ff9e066 100644 --- a/lib/presentation/views/active_game_view.dart +++ b/lib/presentation/views/active_game_view.dart @@ -1,11 +1,16 @@ +import 'package:cabo_counter/core/constants.dart'; import 'package:cabo_counter/core/custom_theme.dart'; import 'package:cabo_counter/data/game_manager.dart'; import 'package:cabo_counter/data/game_session.dart'; import 'package:cabo_counter/l10n/generated/app_localizations.dart'; import 'package:cabo_counter/presentation/views/create_game_view.dart'; import 'package:cabo_counter/presentation/views/graph_view.dart'; +import 'package:cabo_counter/presentation/views/mode_selection_view.dart'; +import 'package:cabo_counter/presentation/views/points_view.dart'; import 'package:cabo_counter/presentation/views/round_view.dart'; import 'package:cabo_counter/services/local_storage_service.dart'; +import 'package:collection/collection.dart'; +import 'package:confetti/confetti.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -19,6 +24,9 @@ class ActiveGameView extends StatefulWidget { } class _ActiveGameViewState extends State { + final confettiController = ConfettiController( + duration: const Duration(seconds: 10), + ); late final GameSession gameSession; late List denseRanks; late List sortedPlayerIndices; @@ -31,200 +39,257 @@ class _ActiveGameViewState extends State { @override Widget build(BuildContext context) { - return ListenableBuilder( - listenable: gameSession, - builder: (context, _) { - sortedPlayerIndices = _getSortedPlayerIndices(); - denseRanks = _calculateDenseRank( - gameSession.playerScores, sortedPlayerIndices); - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: Text(gameSession.gameTitle), - ), - child: SafeArea( - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(10, 10, 0, 0), - child: Text( - AppLocalizations.of(context).players, - style: CustomTheme.rowTitle, - ), - ), - ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: gameSession.players.length, - itemBuilder: (BuildContext context, int index) { - int playerIndex = sortedPlayerIndices[index]; - return CupertinoListTile( - title: Row( - children: [ - _getPlacementTextWidget(index), - const SizedBox(width: 5), - Text( - gameSession.players[playerIndex], - style: const TextStyle( - fontWeight: FontWeight.bold), - ), - ], + return Stack( + children: [ + ListenableBuilder( + listenable: gameSession, + builder: (context, _) { + sortedPlayerIndices = _getSortedPlayerIndices(); + denseRanks = _calculateDenseRank( + gameSession.playerScores, sortedPlayerIndices); + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + middle: Text( + gameSession.gameTitle, + overflow: TextOverflow.ellipsis, + ), + ), + child: SafeArea( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(10, 10, 0, 0), + child: Text( + AppLocalizations.of(context).players, + style: CustomTheme.rowTitle, ), - trailing: Row( - children: [ - const SizedBox(width: 5), - Text('${gameSession.playerScores[playerIndex]} ' - '${AppLocalizations.of(context).points}') - ], - ), - ); - }, - ), - Padding( - padding: const EdgeInsets.fromLTRB(10, 10, 0, 0), - child: Text( - AppLocalizations.of(context).rounds, - style: CustomTheme.rowTitle, - ), - ), - ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: gameSession.roundNumber, - itemBuilder: (BuildContext context, int index) { - return Padding( - padding: const EdgeInsets.all(1), - child: CupertinoListTile( - backgroundColorActivated: - CustomTheme.backgroundColor, - title: Text( - '${AppLocalizations.of(context).round} ${index + 1}', + ), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: gameSession.players.length, + itemBuilder: (BuildContext context, int index) { + int playerIndex = sortedPlayerIndices[index]; + return CupertinoListTile( + title: Row( + children: [ + _getPlacementTextWidget(index), + const SizedBox(width: 5), + Text( + gameSession.players[playerIndex], + style: const TextStyle( + fontWeight: FontWeight.bold), + ), + ], ), - trailing: - index + 1 != gameSession.roundNumber || + trailing: Row( + children: [ + const SizedBox(width: 5), + Text( + '${gameSession.playerScores[playerIndex]} ' + '${AppLocalizations.of(context).points}') + ], + ), + ); + }, + ), + Padding( + padding: const EdgeInsets.fromLTRB(10, 10, 0, 0), + child: Text( + AppLocalizations.of(context).rounds, + style: CustomTheme.rowTitle, + ), + ), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: gameSession.roundNumber, + itemBuilder: (BuildContext context, int index) { + return Padding( + padding: const EdgeInsets.all(1), + child: CupertinoListTile( + backgroundColorActivated: + CustomTheme.backgroundColor, + title: Text( + '${AppLocalizations.of(context).round} ${index + 1}', + ), + trailing: index + 1 != + gameSession.roundNumber || gameSession.isGameFinished == true ? (const Text('\u{2705}', style: TextStyle(fontSize: 22))) : const Text('\u{23F3}', style: TextStyle(fontSize: 22)), - onTap: () async { - _openRoundView(index + 1); - }, - )); - }, - ), - Padding( - padding: const EdgeInsets.fromLTRB(10, 10, 0, 0), - child: Text( - AppLocalizations.of(context).game, - style: CustomTheme.rowTitle, - ), - ), - Column( - children: [ - CupertinoListTile( - title: Text( - AppLocalizations.of(context).game_process, + onTap: () async { + _openRoundView(context, index + 1); + }, + )); + }, + ), + Padding( + padding: const EdgeInsets.fromLTRB(10, 10, 0, 0), + child: Text( + AppLocalizations.of(context).statistics, + style: CustomTheme.rowTitle, + ), + ), + Column( + children: [ + CupertinoListTile( + title: Text( + AppLocalizations.of(context) + .scoring_history, + ), + backgroundColorActivated: + CustomTheme.backgroundColor, + onTap: () => Navigator.push( + context, + CupertinoPageRoute( + builder: (_) => GraphView( + gameSession: gameSession, + )))), + CupertinoListTile( + title: Text( + AppLocalizations.of(context).point_overview, + ), + backgroundColorActivated: + CustomTheme.backgroundColor, + onTap: () => Navigator.push( + context, + CupertinoPageRoute( + builder: (_) => PointsView( + gameSession: gameSession, + )))), + ], + ), + Padding( + padding: const EdgeInsets.fromLTRB(10, 10, 0, 0), + child: Text( + AppLocalizations.of(context).game, + style: CustomTheme.rowTitle, + ), + ), + Column( + children: [ + Visibility( + visible: !gameSession.isPointsLimitEnabled, + child: CupertinoListTile( + title: Text( + AppLocalizations.of(context).end_game, + style: gameSession.roundNumber > 1 && + !gameSession.isGameFinished + ? const TextStyle(color: Colors.white) + : const TextStyle( + color: Colors.white30), + ), + backgroundColorActivated: + CustomTheme.backgroundColor, + onTap: () { + if (gameSession.roundNumber > 1 && + !gameSession.isGameFinished) { + _showEndGameDialog(); + } + }), ), - backgroundColorActivated: - CustomTheme.backgroundColor, - onTap: () => Navigator.push( - context, - CupertinoPageRoute( - builder: (_) => GraphView( - gameSession: gameSession, - )))), - Visibility( - visible: !gameSession.isPointsLimitEnabled, - child: CupertinoListTile( + CupertinoListTile( title: Text( - AppLocalizations.of(context).end_game, - style: gameSession.roundNumber > 1 && - !gameSession.isGameFinished - ? const TextStyle(color: Colors.white) - : const TextStyle(color: Colors.white30), + AppLocalizations.of(context).delete_game, ), backgroundColorActivated: CustomTheme.backgroundColor, onTap: () { - if (gameSession.roundNumber > 1 && - !gameSession.isGameFinished) { - _showEndGameDialog(); - } - }), - ), - CupertinoListTile( - title: Text( - AppLocalizations.of(context).delete_game, - ), - backgroundColorActivated: - CustomTheme.backgroundColor, - onTap: () { - _showDeleteGameDialog().then((value) { - if (value) { - _removeGameSession(gameSession); - } - }); - }, - ), - CupertinoListTile( - title: Text( - AppLocalizations.of(context) - .new_game_same_settings, - ), - backgroundColorActivated: - CustomTheme.backgroundColor, - onTap: () { - Navigator.pushReplacement( - context, - CupertinoPageRoute( - builder: (_) => CreateGameView( - gameTitle: gameSession.gameTitle, - isPointsLimitEnabled: widget - .gameSession - .isPointsLimitEnabled, - players: gameSession.players, - ))); - }, - ), - CupertinoListTile( - title: Text( - AppLocalizations.of(context).export_game, + _showDeleteGameDialog().then((value) { + if (value) { + _removeGameSession(gameSession); + } + }); + }, ), - backgroundColorActivated: - CustomTheme.backgroundColor, - onTap: () async { - final success = await LocalStorageService - .exportSingleGameSession( - widget.gameSession); - if (!success && context.mounted) { - showCupertinoDialog( - context: context, - builder: (context) => CupertinoAlertDialog( - title: Text(AppLocalizations.of(context) - .export_error_title), - content: Text(AppLocalizations.of(context) - .export_error_message), - actions: [ - CupertinoDialogAction( - child: Text( - AppLocalizations.of(context).ok), - onPressed: () => - Navigator.pop(context), + CupertinoListTile( + title: Text( + AppLocalizations.of(context) + .new_game_same_settings, + ), + backgroundColorActivated: + CustomTheme.backgroundColor, + onTap: () { + Navigator.pushReplacement( + context, + CupertinoPageRoute( + builder: (_) => CreateGameView( + gameTitle: + gameSession.gameTitle, + gameMode: widget.gameSession + .isPointsLimitEnabled == + true + ? GameMode.pointLimit + : GameMode.unlimited, + players: gameSession.players, + ))); + }, + ), + CupertinoListTile( + title: Text( + AppLocalizations.of(context).export_game, + ), + backgroundColorActivated: + CustomTheme.backgroundColor, + onTap: () async { + final success = await LocalStorageService + .exportSingleGameSession( + widget.gameSession); + if (!success && context.mounted) { + showCupertinoDialog( + context: context, + builder: (context) => + CupertinoAlertDialog( + title: Text( + AppLocalizations.of(context) + .export_error_title), + content: Text( + AppLocalizations.of(context) + .export_error_message), + actions: [ + CupertinoDialogAction( + child: Text( + AppLocalizations.of(context) + .ok), + onPressed: () => + Navigator.pop(context), + ), + ], ), - ], - ), - ); - } - }), + ); + } + }), + ], + ) ], - ) - ], - ), - ), - )); - }); + ), + ), + )); + }), + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Center( + child: ConfettiWidget( + blastDirectionality: BlastDirectionality.explosive, + particleDrag: 0.07, + emissionFrequency: 0.1, + numberOfParticles: 10, + minBlastForce: 5, + maxBlastForce: 20, + confettiController: confettiController, + ), + ), + ], + ), + ], + ); } /// Shows a dialog to confirm ending the game. @@ -247,6 +312,7 @@ class _ActiveGameViewState extends State { onPressed: () { setState(() { gameManager.endGame(gameSession.id); + _playFinishAnimation(context); }); Navigator.pop(context); }, @@ -376,8 +442,8 @@ class _ActiveGameViewState extends State { /// Recursively opens the RoundView for the specified round number. /// It starts with the given [roundNumber] and continues to open the next round /// until the user navigates back or the round number is invalid. - void _openRoundView(int roundNumber) async { - final val = await Navigator.of(context, rootNavigator: true).push( + void _openRoundView(BuildContext context, int roundNumber) async { + final round = await Navigator.of(context, rootNavigator: true).push( CupertinoPageRoute( fullscreenDialog: true, builder: (context) => RoundView( @@ -386,11 +452,58 @@ class _ActiveGameViewState extends State { ), ), ); - if (val != null && val >= 0) { + + if (widget.gameSession.isGameFinished && context.mounted) { + _playFinishAnimation(context); + } + + // If the previous round was not the last one + if (round != null && round >= 0) { WidgetsBinding.instance.addPostFrameCallback((_) async { - await Future.delayed(const Duration(milliseconds: 600)); - _openRoundView(val); + await Future.delayed( + const Duration(milliseconds: Constants.roundViewDelay)); + if (context.mounted) { + _openRoundView(context, round); + } }); } } + + /// Plays the confetti animation and shows a dialog with the winner's information. + Future _playFinishAnimation(BuildContext context) async { + String winner = widget.gameSession.winner; + int winnerPoints = widget.gameSession.playerScores.min; + int winnerAmount = winner.contains('&') ? 2 : 1; + + confettiController.play(); + + await Future.delayed(const Duration(milliseconds: Constants.popUpDelay)); + + if (context.mounted) { + showCupertinoDialog( + context: context, + builder: (BuildContext context) { + return CupertinoAlertDialog( + title: Text(AppLocalizations.of(context).end_of_game_title), + content: Text(AppLocalizations.of(context) + .end_of_game_message(winnerAmount, winner, winnerPoints)), + actions: [ + CupertinoDialogAction( + child: Text(AppLocalizations.of(context).ok), + onPressed: () { + confettiController.stop(); + Navigator.pop(context); + }, + ), + ], + ); + }); + } + } + + @override + void dispose() { + confettiController.dispose(); + super.dispose(); + } } diff --git a/lib/presentation/views/create_game_view.dart b/lib/presentation/views/create_game_view.dart index 0d23494..193cdff 100644 --- a/lib/presentation/views/create_game_view.dart +++ b/lib/presentation/views/create_game_view.dart @@ -1,11 +1,16 @@ +import 'package:cabo_counter/core/constants.dart'; import 'package:cabo_counter/core/custom_theme.dart'; import 'package:cabo_counter/data/game_manager.dart'; import 'package:cabo_counter/data/game_session.dart'; import 'package:cabo_counter/l10n/generated/app_localizations.dart'; import 'package:cabo_counter/presentation/views/active_game_view.dart'; import 'package:cabo_counter/presentation/views/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, @@ -16,15 +21,15 @@ enum CreateStatus { } class CreateGameView extends StatefulWidget { + final GameMode gameMode; final String? gameTitle; - final bool? isPointsLimitEnabled; final List? players; const CreateGameView({ super.key, this.gameTitle, - this.isPointsLimitEnabled, this.players, + required this.gameMode, }); @override @@ -33,29 +38,39 @@ class CreateGameView extends StatefulWidget { } class _CreateGameViewState extends State { + final TextEditingController _gameTitleTextController = + TextEditingController(); + + /// List of text controllers for player names. final List _playerNameTextControllers = [ TextEditingController() ]; - final TextEditingController _gameTitleTextController = - 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; - /// Variable to store whether the points limit feature is enabled. - bool? _isPointsLimitEnabled; + /// 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(); - _isPointsLimitEnabled = widget.isPointsLimitEnabled; + 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()); } } } @@ -63,124 +78,100 @@ class _CreateGameViewState extends State { @override Widget build(BuildContext context) { return CupertinoPageScaffold( + resizeToAvoidBottomInset: false, navigationBar: CupertinoNavigationBar( previousPageTitle: AppLocalizations.of(context).overview, middle: Text(AppLocalizations.of(context).new_game), ), child: SafeArea( - child: Center( - 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: 16, - prefix: Text(AppLocalizations.of(context).name), - textAlign: TextAlign.right, - placeholder: AppLocalizations.of(context).game_title, - controller: _gameTitleTextController, - ), - ), - // Spielmodus-Auswahl mit Chevron - 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: [ - Text( - _isPointsLimitEnabled == null - ? AppLocalizations.of(context).select_mode - : (_isPointsLimitEnabled! - ? '${ConfigService.pointLimit} ${AppLocalizations.of(context).points}' - : AppLocalizations.of(context).unlimited), - ), - const SizedBox(width: 3), - const CupertinoListTileChevron(), - ], + 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, ), - onTap: () async { - final selectedMode = await Navigator.push( - context, - CupertinoPageRoute( - builder: (context) => ModeSelectionMenu( - pointLimit: ConfigService.pointLimit, - ), - ), - ); - - if (selectedMode != null) { - setState(() { - _isPointsLimitEnabled = selectedMode; - }); - } - }, ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(10, 10, 0, 0), - child: Text( - AppLocalizations.of(context).players, - 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, + ), ), - ), - Expanded( - child: ListView.builder( - itemCount: _playerNameTextControllers.length + - 1, // +1 für den + Button - itemBuilder: (context, index) { - if (index == _playerNameTextControllers.length) { - // + Button als letztes Element - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: CupertinoButton( - padding: EdgeInsets.zero, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon( - CupertinoIcons.add_circled, - color: CupertinoColors.activeGreen, - size: 25, - ), - const SizedBox(width: 8), - Text( - AppLocalizations.of(context).add_player, - style: const TextStyle( - color: CupertinoColors.activeGreen, - ), - ), - ], + 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 { + final selectedMode = await Navigator.push( + context, + CupertinoPageRoute( + builder: (context) => ModeSelectionMenu( + pointLimit: ConfigService.getPointLimit(), + showDeselection: false, ), - onPressed: () { - if (_playerNameTextControllers.length < maxPlayers) { - setState(() { - _playerNameTextControllers - .add(TextEditingController()); - }); - } else { - showFeedbackDialog(CreateStatus.maxPlayers); - } - }, ), ); - } else { - // Spieler-Einträge + + 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( - padding: const EdgeInsets.symmetric( - vertical: 8.0, horizontal: 5), + key: ValueKey(index), + padding: const EdgeInsets.symmetric(vertical: 8.0), child: Row( children: [ CupertinoButton( @@ -200,82 +191,187 @@ class _CreateGameViewState extends State { 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.fadeInDuration), + child: Padding( + padding: const EdgeInsets.only(right: 8.0), + child: ReorderableDragStartListener( + index: index, + child: const Icon( + CupertinoIcons.line_horizontal_3, + color: CupertinoColors.systemGrey, + ), + ), + ), + ) ], ), ); - } - }, - ), - ), - Center( - child: CupertinoButton( - padding: EdgeInsets.zero, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, + }), + Padding( + padding: const EdgeInsets.fromLTRB(8, 0, 8, 50), + child: Stack( children: [ - Text( - AppLocalizations.of(context).create_game, - style: const TextStyle( - color: CupertinoColors.activeGreen, + 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); + } + }, ), ), ], ), - onPressed: () async { - if (_gameTitleTextController.text == '') { - showFeedbackDialog(CreateStatus.noGameTitle); - return; - } - if (_isPointsLimitEnabled == null) { - showFeedbackDialog(CreateStatus.noModeSelected); - return; - } - if (_playerNameTextControllers.length < 2) { - showFeedbackDialog(CreateStatus.minPlayers); - return; - } - if (!everyPlayerHasAName()) { - showFeedbackDialog(CreateStatus.noPlayerName); - return; - } - - List players = []; - for (var controller in _playerNameTextControllers) { - players.add(controller.text); - } - GameSession gameSession = GameSession( - createdAt: DateTime.now(), - gameTitle: _gameTitleTextController.text, - players: players, - pointLimit: ConfigService.pointLimit, - caboPenalty: ConfigService.caboPenalty, - isPointsLimitEnabled: _isPointsLimitEnabled!, - ); - final index = await gameManager.addGameSession(gameSession); - final session = gameManager.gameList[index]; - if (context.mounted) { - Navigator.pushReplacement( - context, - CupertinoPageRoute( - builder: (context) => - ActiveGameView(gameSession: session))); - } - }, ), - ), - ], - )))); + Padding( + padding: const EdgeInsets.fromLTRB(0, 0, 0, 50), + child: Center( + key: const ValueKey('create_game_button'), + child: CustomButton( + child: Text( + AppLocalizations.of(context).create_game, + style: TextStyle( + color: CustomTheme.primaryColor, + ), + ), + onPressed: () { + _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) { + void _showFeedbackDialog(CreateStatus status) { final (title, message) = _getDialogContent(status); showCupertinoDialog( @@ -326,15 +422,36 @@ class _CreateGameViewState extends State { } } - /// Checks if every player has a name. - /// Returns true if all players have a name, false otherwise. - bool everyPlayerHasAName() { + /// 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) { - if (controller.text == '') { - return false; - } + players.add(controller.text); } - return true; + + 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.pushReplacement( + context, + CupertinoPageRoute( + builder: (context) => ActiveGameView(gameSession: session))); } @override @@ -343,6 +460,9 @@ class _CreateGameViewState extends State { for (var controller in _playerNameTextControllers) { controller.dispose(); } + for (var focusnode in _playerNameFocusNodes) { + focusnode.dispose(); + } super.dispose(); } diff --git a/lib/presentation/views/graph_view.dart b/lib/presentation/views/graph_view.dart index d322bd0..da9f8e7 100644 --- a/lib/presentation/views/graph_view.dart +++ b/lib/presentation/views/graph_view.dart @@ -27,46 +27,61 @@ class _GraphViewState extends State { Widget build(BuildContext context) { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( - middle: Text(AppLocalizations.of(context).game_process), + middle: Text(AppLocalizations.of(context).scoring_history), previousPageTitle: AppLocalizations.of(context).back, ), - child: widget.gameSession.roundNumber > 1 - ? Padding( - padding: const EdgeInsets.fromLTRB(0, 100, 0, 0), - child: SfCartesianChart( - legend: const Legend( - overflowMode: LegendItemOverflowMode.wrap, - isVisible: true, - position: LegendPosition.bottom), - primaryXAxis: const NumericAxis( - interval: 1, - decimalPlaces: 0, - ), - primaryYAxis: const NumericAxis( - interval: 1, - decimalPlaces: 0, - ), - series: getCumulativeScores(), + child: Visibility( + visible: widget.gameSession.roundNumber > 1 || + widget.gameSession.isGameFinished, + replacement: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Center( + child: Icon(CupertinoIcons.chart_bar_alt_fill, size: 60), + ), + const SizedBox(height: 10), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 40), + child: Text( + AppLocalizations.of(context).empty_graph_text, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 16), ), - ) - : Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Center( - child: Icon(CupertinoIcons.chart_bar_alt_fill, size: 60), - ), - const SizedBox(height: 10), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 40), - child: Text( - AppLocalizations.of(context).empty_graph_text, - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 16), - ), - ), - ], - )); + ), + ], + ), + child: Padding( + padding: const EdgeInsets.fromLTRB(0, 100, 0, 0), + child: SfCartesianChart( + enableAxisAnimation: true, + legend: const Legend( + overflowMode: LegendItemOverflowMode.wrap, + isVisible: true, + position: LegendPosition.bottom), + primaryXAxis: const NumericAxis( + labelStyle: TextStyle(fontWeight: FontWeight.bold), + interval: 1, + decimalPlaces: 0, + ), + primaryYAxis: NumericAxis( + labelStyle: const TextStyle(fontWeight: FontWeight.bold), + labelAlignment: LabelAlignment.center, + labelPosition: ChartDataLabelPosition.inside, + interval: 1, + decimalPlaces: 0, + axisLabelFormatter: (AxisLabelRenderDetails details) { + if (details.value == 0) { + return ChartAxisLabel('', const TextStyle()); + } + return ChartAxisLabel( + '${details.value.toInt()}', const TextStyle()); + }, + ), + series: getCumulativeScores(), + ), + ), + )); } /// Returns a list of LineSeries representing the cumulative scores of each player. diff --git a/lib/presentation/views/main_menu_view.dart b/lib/presentation/views/main_menu_view.dart index 86ff208..21deb97 100644 --- a/lib/presentation/views/main_menu_view.dart +++ b/lib/presentation/views/main_menu_view.dart @@ -58,162 +58,167 @@ class _MainMenuViewState extends State { listenable: gameManager, builder: (context, _) { return CupertinoPageScaffold( - resizeToAvoidBottomInset: false, - navigationBar: CupertinoNavigationBar( - leading: IconButton( - onPressed: () { - Navigator.push( - context, - CupertinoPageRoute( - builder: (context) => const SettingsView(), - ), - ).then((_) { - setState(() {}); - }); - }, - icon: const Icon(CupertinoIcons.settings, size: 30)), - middle: const Text('Cabo Counter'), - trailing: IconButton( - onPressed: () => Navigator.push( + resizeToAvoidBottomInset: false, + navigationBar: CupertinoNavigationBar( + leading: IconButton( + onPressed: () { + Navigator.push( context, CupertinoPageRoute( - builder: (context) => const CreateGameView(), + builder: (context) => const SettingsView(), ), + ).then((_) { + setState(() {}); + }); + }, + icon: const Icon(CupertinoIcons.settings, size: 30)), + middle: Text(AppLocalizations.of(context).app_name), + trailing: IconButton( + onPressed: () => Navigator.push( + context, + CupertinoPageRoute( + builder: (context) => CreateGameView( + gameMode: ConfigService.getGameMode()), + ), + ), + icon: const Icon(CupertinoIcons.add)), + ), + child: CupertinoPageScaffold( + child: SafeArea( + child: Visibility( + visible: _isLoading, + replacement: Visibility( + visible: gameManager.gameList.isEmpty, + replacement: ListView.separated( + itemCount: gameManager.gameList.length, + separatorBuilder: (context, index) => Divider( + height: 1, + thickness: 0.5, + color: CustomTheme.white.withAlpha(50), + indent: 50, + endIndent: 50, ), - icon: const Icon(CupertinoIcons.add)), - ), - child: CupertinoPageScaffold( - child: SafeArea( - child: _isLoading - ? const Center(child: CupertinoActivityIndicator()) - : gameManager.gameList.isEmpty - ? Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox(height: 30), - Center( - child: GestureDetector( - onTap: () => Navigator.push( - context, - CupertinoPageRoute( - builder: (context) => - const CreateGameView(), + itemBuilder: (context, index) { + final session = gameManager.gameList[index]; + return ListenableBuilder( + listenable: session, + builder: (context, _) { + return Dismissible( + key: Key(session.id), + background: Container( + color: CupertinoColors.destructiveRed, + alignment: Alignment.centerRight, + padding: const EdgeInsets.only(right: 20.0), + child: const Icon( + CupertinoIcons.delete, + color: CupertinoColors.white, ), ), - child: Icon( - CupertinoIcons.plus, - size: 60, - color: CustomTheme.primaryColor, - ), - )), - const SizedBox(height: 10), - Padding( - padding: - const EdgeInsets.symmetric(horizontal: 70), - child: Text( - '${AppLocalizations.of(context).empty_text_1}\n${AppLocalizations.of(context).empty_text_2}', - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 16), - ), - ), - ], - ) - : ListView.builder( - itemCount: gameManager.gameList.length, - itemBuilder: (context, index) { - final session = gameManager.gameList[index]; - return ListenableBuilder( - listenable: session, - builder: (context, _) { - return Dismissible( - key: Key(session.gameTitle), - background: Container( - color: CupertinoColors.destructiveRed, - alignment: Alignment.centerRight, - padding: - const EdgeInsets.only(right: 20.0), - child: const Icon( - CupertinoIcons.delete, - color: CupertinoColors.white, + direction: DismissDirection.endToStart, + confirmDismiss: (direction) async { + return await _showDeleteGamePopup( + context, session.gameTitle); + }, + onDismissed: (direction) { + gameManager.removeGameSessionById(session.id); + }, + dismissThresholds: const { + DismissDirection.startToEnd: 0.6 + }, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 10.0), + child: CupertinoListTile( + backgroundColorActivated: + CustomTheme.backgroundColor, + title: Text(session.gameTitle), + subtitle: Visibility( + visible: session.isGameFinished, + replacement: Text( + '${AppLocalizations.of(context).mode}: ${_translateGameMode(session.isPointsLimitEnabled)}', + style: const TextStyle(fontSize: 14), ), - ), - direction: DismissDirection.endToStart, - confirmDismiss: (direction) async { - final String gameTitle = gameManager - .gameList[index].gameTitle; - return await _showDeleteGamePopup( - context, gameTitle); - }, - onDismissed: (direction) { - gameManager - .removeGameSessionByIndex(index); - }, - dismissThresholds: const { - DismissDirection.startToEnd: 0.6 - }, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 10.0), - child: CupertinoListTile( - backgroundColorActivated: - CustomTheme.backgroundColor, - title: Text(session.gameTitle), - subtitle: - session.isGameFinished == true - ? Text( - '\u{1F947} ${session.winner}', - style: const TextStyle( - fontSize: 14), - ) - : Text( - '${AppLocalizations.of(context).mode}: ${_translateGameMode(session.isPointsLimitEnabled)}', - style: const TextStyle( - fontSize: 14), - ), - trailing: Row( - children: [ - Text('${session.roundNumber}'), - const SizedBox(width: 3), - const Icon(CupertinoIcons - .arrow_2_circlepath_circle_fill), - const SizedBox(width: 15), - Text('${session.players.length}'), - const SizedBox(width: 3), - const Icon( - CupertinoIcons.person_2_fill), - ], - ), - onTap: () { - final session = - gameManager.gameList[index]; - Navigator.push( - context, - CupertinoPageRoute( - builder: (context) => - ActiveGameView( - gameSession: session), - ), - ).then((_) { - setState(() {}); - }); - }, + child: Text( + '\u{1F947} ${session.winner}', + style: const TextStyle(fontSize: 14), + )), + trailing: Row( + children: [ + const SizedBox( + width: 5, ), - ), - ); - }); - }, + Text('${session.roundNumber}'), + const SizedBox(width: 3), + const Icon(CupertinoIcons + .arrow_2_circlepath_circle_fill), + const SizedBox(width: 15), + Text('${session.players.length}'), + const SizedBox(width: 3), + const Icon( + CupertinoIcons.person_2_fill), + ], + ), + onTap: () { + final session = + gameManager.gameList[index]; + Navigator.push( + context, + CupertinoPageRoute( + builder: (context) => ActiveGameView( + gameSession: session), + ), + ).then((_) { + setState(() {}); + }); + }, + ), + ), + ); + }); + }, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 30), + Center( + child: GestureDetector( + onTap: () => Navigator.push( + context, + CupertinoPageRoute( + builder: (context) => CreateGameView( + gameMode: ConfigService.getGameMode()), + ), ), - ), - ), - ); + child: Icon( + CupertinoIcons.plus, + size: 60, + color: CustomTheme.primaryColor, + ), + )), + const SizedBox(height: 10), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 70), + child: Text( + '${AppLocalizations.of(context).empty_text_1}\n${AppLocalizations.of(context).empty_text_2}', + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 16), + ), + ), + ], + ), + ), + child: const Center(child: CupertinoActivityIndicator()), + ), + ))); }); } /// Translates the game mode boolean into the corresponding String. /// If [pointLimit] is true, it returns '101 Punkte', otherwise it returns 'Unbegrenzt'. - String _translateGameMode(bool pointLimit) { - if (pointLimit) { - return '${ConfigService.pointLimit} ${AppLocalizations.of(context).points}'; + String _translateGameMode(bool isPointLimitEnabled) { + if (isPointLimitEnabled) { + return '${ConfigService.getPointLimit()} ${AppLocalizations.of(context).points}'; } return AppLocalizations.of(context).unlimited; } @@ -237,7 +242,7 @@ class _MainMenuViewState extends State { BadRatingDialogDecision badRatingDecision = BadRatingDialogDecision.cancel; // so that the bad rating dialog is not shown immediately - await Future.delayed(const Duration(milliseconds: 300)); + await Future.delayed(const Duration(milliseconds: Constants.popUpDelay)); switch (preRatingDecision) { case PreRatingDialogDecision.yes: diff --git a/lib/presentation/views/mode_selection_view.dart b/lib/presentation/views/mode_selection_view.dart index a7d3ce7..0424dab 100644 --- a/lib/presentation/views/mode_selection_view.dart +++ b/lib/presentation/views/mode_selection_view.dart @@ -2,9 +2,17 @@ import 'package:cabo_counter/core/custom_theme.dart'; import 'package:cabo_counter/l10n/generated/app_localizations.dart'; import 'package:flutter/cupertino.dart'; +enum GameMode { + none, + pointLimit, + unlimited, +} + class ModeSelectionMenu extends StatelessWidget { final int pointLimit; - const ModeSelectionMenu({super.key, required this.pointLimit}); + final bool showDeselection; + const ModeSelectionMenu( + {super.key, required this.pointLimit, required this.showDeselection}); @override Widget build(BuildContext context) { @@ -26,12 +34,12 @@ class ModeSelectionMenu extends StatelessWidget { maxLines: 3, ), onTap: () { - Navigator.pop(context, true); + Navigator.pop(context, GameMode.pointLimit); }, ), ), Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), + padding: const EdgeInsets.fromLTRB(0, 16, 0, 0), child: CupertinoListTile( title: Text(AppLocalizations.of(context).unlimited, style: CustomTheme.modeTitle), @@ -41,10 +49,27 @@ class ModeSelectionMenu extends StatelessWidget { maxLines: 3, ), onTap: () { - Navigator.pop(context, false); + Navigator.pop(context, GameMode.unlimited); }, ), ), + Visibility( + visible: showDeselection, + child: Padding( + padding: const EdgeInsets.fromLTRB(0, 16, 0, 0), + child: CupertinoListTile( + title: Text(AppLocalizations.of(context).no_default_mode, + style: CustomTheme.modeTitle), + subtitle: Text( + AppLocalizations.of(context).no_default_description, + style: CustomTheme.modeDescription, + maxLines: 3, + ), + onTap: () { + Navigator.pop(context, GameMode.none); + }, + ), + )), ], ), ); diff --git a/lib/presentation/views/points_view.dart b/lib/presentation/views/points_view.dart new file mode 100644 index 0000000..1379785 --- /dev/null +++ b/lib/presentation/views/points_view.dart @@ -0,0 +1,141 @@ +import 'package:cabo_counter/core/custom_theme.dart'; +import 'package:cabo_counter/data/game_session.dart'; +import 'package:cabo_counter/l10n/generated/app_localizations.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class PointsView extends StatefulWidget { + final GameSession gameSession; + + const PointsView({super.key, required this.gameSession}); + + @override + State createState() => _PointsViewState(); +} + +class _PointsViewState extends State { + @override + Widget build(BuildContext context) { + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + middle: Text(AppLocalizations.of(context).point_overview), + previousPageTitle: AppLocalizations.of(context).back, + ), + child: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(0, 100, 0, 0), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: DataTable( + dataRowMinHeight: 60, + dataRowMaxHeight: 60, + dividerThickness: 0.5, + columnSpacing: 20, + columns: [ + const DataColumn( + numeric: true, + headingRowAlignment: MainAxisAlignment.center, + label: Text( + '#', + style: TextStyle(fontWeight: FontWeight.bold), + ), + columnWidth: IntrinsicColumnWidth(flex: 0.5)), + ...widget.gameSession.players.map( + (player) => DataColumn( + label: FittedBox( + fit: BoxFit.fill, + child: Text( + player, + style: const TextStyle(fontWeight: FontWeight.bold), + )), + headingRowAlignment: MainAxisAlignment.center, + columnWidth: const IntrinsicColumnWidth(flex: 1)), + ), + ], + rows: [ + ...List.generate( + widget.gameSession.roundList.length, + (roundIndex) { + final round = widget.gameSession.roundList[roundIndex]; + return DataRow( + cells: [ + DataCell(Align( + alignment: Alignment.center, + child: Text( + '${roundIndex + 1}', + style: const TextStyle(fontSize: 20), + ), + )), + ...List.generate(widget.gameSession.players.length, + (playerIndex) { + final int score = round.scores[playerIndex]; + final int update = round.scoreUpdates[playerIndex]; + final bool saidCabo = + round.caboPlayerIndex == playerIndex; + return DataCell( + Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: update <= 0 + ? CustomTheme.pointLossColor + : CustomTheme.pointGainColor, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + '${update >= 0 ? '+' : ''}$update', + style: const TextStyle( + color: CupertinoColors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(height: 4), + Text('$score', + style: TextStyle( + fontWeight: saidCabo + ? FontWeight.bold + : FontWeight.normal, + )), + ], + ), + ), + ); + }), + ], + ); + }, + ), + DataRow( + cells: [ + const DataCell(Align( + alignment: Alignment.center, + child: Text( + 'Σ', + style: + TextStyle(fontSize: 25, fontWeight: FontWeight.bold), + ), + )), + ...widget.gameSession.playerScores.map( + (score) => DataCell( + Center( + child: Text( + '$score', + style: const TextStyle( + fontSize: 20, fontWeight: FontWeight.bold), + ), + ), + ), + ), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/presentation/views/round_view.dart b/lib/presentation/views/round_view.dart index 7c62120..d2a9da5 100644 --- a/lib/presentation/views/round_view.dart +++ b/lib/presentation/views/round_view.dart @@ -1,6 +1,7 @@ import 'package:cabo_counter/core/custom_theme.dart'; import 'package:cabo_counter/data/game_session.dart'; import 'package:cabo_counter/l10n/generated/app_localizations.dart'; +import 'package:cabo_counter/presentation/widgets/custom_button.dart'; import 'package:cabo_counter/services/local_storage_service.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart'; @@ -74,21 +75,22 @@ class _RoundViewState extends State { return CupertinoPageScaffold( resizeToAvoidBottomInset: false, navigationBar: CupertinoNavigationBar( - transitionBetweenRoutes: true, - leading: CupertinoButton( - padding: EdgeInsets.zero, - onPressed: () => - {LocalStorageService.saveGameSessions(), Navigator.pop(context)}, - child: Text(AppLocalizations.of(context).cancel), - ), - middle: Text(AppLocalizations.of(context).results), - trailing: widget.gameSession.isGameFinished - ? const Icon( + transitionBetweenRoutes: true, + leading: CupertinoButton( + padding: EdgeInsets.zero, + onPressed: () => { + LocalStorageService.saveGameSessions(), + Navigator.pop(context) + }, + child: Text(AppLocalizations.of(context).cancel), + ), + middle: Text(AppLocalizations.of(context).results), + trailing: Visibility( + visible: widget.gameSession.isGameFinished, + child: const Icon( CupertinoIcons.lock, size: 25, - ) - : null, - ), + ))), child: Stack( children: [ Positioned.fill( @@ -114,9 +116,10 @@ class _RoundViewState extends State { vertical: 10, ), child: SizedBox( - height: 40, + height: 60, child: CupertinoSegmentedControl( - unselectedColor: CustomTheme.backgroundTintColor, + unselectedColor: + CustomTheme.mainElementBackgroundColor, selectedColor: CustomTheme.primaryColor, groupValue: _caboPlayerIndex, children: Map.fromEntries(widget.gameSession.players @@ -130,7 +133,7 @@ class _RoundViewState extends State { Padding( padding: const EdgeInsets.symmetric( horizontal: 6, - vertical: 6, + vertical: 8, ), child: FittedBox( fit: BoxFit.scaleDown, @@ -154,27 +157,6 @@ class _RoundViewState extends State { ), ), ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20.0), - child: CupertinoListTile( - title: Text(AppLocalizations.of(context).player), - trailing: Row( - children: [ - SizedBox( - width: 100, - child: Center( - child: Text( - AppLocalizations.of(context).points))), - const SizedBox(width: 20), - SizedBox( - width: 80, - child: Center( - child: Text(AppLocalizations.of(context) - .kamikaze))), - ], - ), - ), - ), ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), @@ -182,13 +164,15 @@ class _RoundViewState extends State { itemBuilder: (context, index) { final originalIndex = originalIndices[index]; final name = rotatedPlayers[index]; + bool shouldShowMedal = + index == 0 && widget.roundNumber > 1; return Padding( padding: const EdgeInsets.symmetric( vertical: 10, horizontal: 20), child: ClipRRect( borderRadius: BorderRadius.circular(12), child: CupertinoListTile( - backgroundColor: CupertinoColors.secondaryLabel, + backgroundColor: CustomTheme.playerTileColor, title: Row(children: [ Expanded( child: Row(children: [ @@ -197,95 +181,70 @@ class _RoundViewState extends State { overflow: TextOverflow.ellipsis, ), Visibility( - visible: index == 0, + visible: shouldShowMedal, child: const SizedBox(width: 10), ), Visibility( - visible: index == 0, - child: const Icon(FontAwesomeIcons.medal, + visible: shouldShowMedal, + child: const Icon(FontAwesomeIcons.crown, size: 15)) ])) ]), subtitle: Text( '${widget.gameSession.playerScores[originalIndex]}' ' ${AppLocalizations.of(context).points}'), - trailing: Row( - children: [ - SizedBox( - width: 100, - child: CupertinoTextField( - maxLength: 3, - focusNode: _focusNodeList[originalIndex], - keyboardType: - const TextInputType.numberWithOptions( - signed: true, - decimal: false, - ), - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - ], - textInputAction: index == - widget.gameSession.players - .length - - 1 - ? TextInputAction.done - : TextInputAction.next, - controller: - _scoreControllerList[originalIndex], - placeholder: - AppLocalizations.of(context).points, - textAlign: TextAlign.center, - onSubmitted: (_) => - _focusNextTextfield(originalIndex), - onChanged: (_) => setState(() {}), - ), + trailing: SizedBox( + width: 100, + child: CupertinoTextField( + maxLength: 3, + focusNode: _focusNodeList[originalIndex], + keyboardType: + const TextInputType.numberWithOptions( + signed: true, + decimal: false, ), - const SizedBox(width: 50), - GestureDetector( - onTap: () { - setState(() { - _kamikazePlayerIndex = - (_kamikazePlayerIndex == - originalIndex) - ? null - : originalIndex; - }); - }, - child: Container( - width: 24, - height: 24, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: _kamikazePlayerIndex == - originalIndex - ? CupertinoColors.systemRed - : CupertinoColors - .tertiarySystemFill, - border: Border.all( - color: _kamikazePlayerIndex == - originalIndex - ? CupertinoColors.systemRed - : CupertinoColors.systemGrey, - ), - ), - child: _kamikazePlayerIndex == - originalIndex - ? const Icon( - CupertinoIcons.exclamationmark, - size: 16, - color: CupertinoColors.white, - ) - : null, - ), - ), - const SizedBox(width: 22), - ], + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], + textInputAction: index == + widget.gameSession.players.length - 1 + ? TextInputAction.done + : TextInputAction.next, + controller: + _scoreControllerList[originalIndex], + placeholder: + AppLocalizations.of(context).points, + textAlign: TextAlign.center, + onSubmitted: (_) => + _focusNextTextfield(originalIndex), + onChanged: (_) => setState(() {}), + ), ), ), ), ); }, ), + Padding( + padding: const EdgeInsets.fromLTRB(0, 10, 0, 0), + child: Center( + heightFactor: 1, + child: CustomButton( + onPressed: () async { + if (await _showKamikazeSheet(context)) { + if (!context.mounted) return; + _endOfRoundNavigation(context, true); + } + }, + child: Text( + AppLocalizations.of(context).kamikaze, + style: const TextStyle( + color: CupertinoColors.destructiveRed, + ), + ), + ), + ), + ), ], ), ), @@ -300,21 +259,14 @@ class _RoundViewState extends State { return Container( height: 80, padding: const EdgeInsets.only(bottom: 20), - color: CustomTheme.backgroundTintColor, + color: CustomTheme.mainElementBackgroundColor, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ CupertinoButton( onPressed: _areRoundInputsValid() - ? () async { - List bonusPlayersIndices = _finishRound(); - if (bonusPlayersIndices.isNotEmpty) { - await _showBonusPopup( - context, bonusPlayersIndices); - } - LocalStorageService.saveGameSessions(); - if (!context.mounted) return; - Navigator.pop(context); + ? () { + _endOfRoundNavigation(context, false); } : null, child: Text(AppLocalizations.of(context).done), @@ -322,21 +274,8 @@ class _RoundViewState extends State { if (!widget.gameSession.isGameFinished) CupertinoButton( onPressed: _areRoundInputsValid() - ? () async { - List bonusPlayersIndices = - _finishRound(); - if (bonusPlayersIndices.isNotEmpty) { - await _showBonusPopup( - context, bonusPlayersIndices); - } - LocalStorageService.saveGameSessions(); - if (widget.gameSession.isGameFinished && - context.mounted) { - Navigator.pop(context); - } else if (context.mounted) { - Navigator.pop( - context, widget.roundNumber + 1); - } + ? () { + _endOfRoundNavigation(context, true); } : null, child: Text(AppLocalizations.of(context).next_round), @@ -399,6 +338,37 @@ class _RoundViewState extends State { ]; } + /// Shows a Cupertino action sheet to select the player who has Kamikaze. + /// It returns true if a player was selected, false if the action was cancelled. + Future _showKamikazeSheet(BuildContext context) async { + return await showCupertinoModalPopup( + context: context, + builder: (BuildContext context) { + return CupertinoActionSheet( + title: Text(AppLocalizations.of(context).kamikaze), + message: Text(AppLocalizations.of(context).who_has_kamikaze), + actions: widget.gameSession.players.asMap().entries.map((entry) { + final index = entry.key; + final name = entry.value; + return CupertinoActionSheetAction( + onPressed: () { + _kamikazePlayerIndex = index; + Navigator.pop(context, true); + }, + child: Text(name), + ); + }).toList(), + cancelButton: CupertinoActionSheetAction( + onPressed: () => Navigator.pop(context, false), + isDestructiveAction: true, + child: Text(AppLocalizations.of(context).cancel), + ), + ); + }, + ) ?? + false; + } + /// Focuses the next text field in the list of text fields. /// [index] is the index of the current text field. void _focusNextTextfield(int index) { @@ -469,10 +439,9 @@ class _RoundViewState extends State { return bonusPlayers; } - /// Shows a popup dialog with the bonus information. + /// Shows a popup dialog with the information which player received the bonus points. Future _showBonusPopup( BuildContext context, List bonusPlayers) async { - print('Bonus Popup wird angezeigt'); int pointLimit = widget.gameSession.pointLimit; int bonusPoints = (pointLimit / 2).round(); @@ -519,6 +488,37 @@ class _RoundViewState extends State { return resultText; } + /// Handles the navigation for the end of the round. + /// It checks for bonus players and shows a popup, saves the game session, + /// and navigates to the next round or back to the previous screen. + /// It takes the BuildContext [context] and a boolean [navigateToNextRound] to determine + /// if it should navigate to the next round or not. + Future _endOfRoundNavigation( + BuildContext context, bool navigateToNextRound) async { + List bonusPlayersIndices = _finishRound(); + if (bonusPlayersIndices.isNotEmpty) { + await _showBonusPopup(context, bonusPlayersIndices); + } + + LocalStorageService.saveGameSessions(); + + if (context.mounted) { + // If the game is finished, pop the context and return to the previous screen. + if (widget.gameSession.isGameFinished) { + Navigator.pop(context); + return; + } + // If navigateToNextRound is false, pop the context and return to the previous screen. + if (!navigateToNextRound) { + Navigator.pop(context); + return; + } + // If navigateToNextRound is true and the game isn't finished yet, + // pop the context and navigate to the next round. + Navigator.pop(context, widget.roundNumber + 1); + } + } + @override void dispose() { for (final controller in _scoreControllerList) { diff --git a/lib/presentation/views/settings_view.dart b/lib/presentation/views/settings_view.dart index d6f0833..aa49872 100644 --- a/lib/presentation/views/settings_view.dart +++ b/lib/presentation/views/settings_view.dart @@ -1,6 +1,7 @@ import 'package:cabo_counter/core/constants.dart'; import 'package:cabo_counter/core/custom_theme.dart'; import 'package:cabo_counter/l10n/generated/app_localizations.dart'; +import 'package:cabo_counter/presentation/views/mode_selection_view.dart'; import 'package:cabo_counter/presentation/widgets/custom_form_row.dart'; import 'package:cabo_counter/presentation/widgets/custom_stepper.dart'; import 'package:cabo_counter/services/config_service.dart'; @@ -20,6 +21,7 @@ class SettingsView extends StatefulWidget { class _SettingsViewState extends State { UniqueKey _stepperKey1 = UniqueKey(); UniqueKey _stepperKey2 = UniqueKey(); + GameMode defaultMode = ConfigService.getGameMode(); @override void initState() { super.initState(); @@ -55,14 +57,13 @@ class _SettingsViewState extends State { prefixIcon: CupertinoIcons.bolt_fill, suffixWidget: CustomStepper( key: _stepperKey1, - initialValue: ConfigService.caboPenalty, + initialValue: ConfigService.getCaboPenalty(), minValue: 0, maxValue: 50, step: 1, onChanged: (newCaboPenalty) { setState(() { ConfigService.setCaboPenalty(newCaboPenalty); - ConfigService.caboPenalty = newCaboPenalty; }); }, ), @@ -72,18 +73,51 @@ class _SettingsViewState extends State { prefixIcon: FontAwesomeIcons.bullseye, suffixWidget: CustomStepper( key: _stepperKey2, - initialValue: ConfigService.pointLimit, + initialValue: ConfigService.getPointLimit(), minValue: 30, maxValue: 1000, step: 10, onChanged: (newPointLimit) { setState(() { ConfigService.setPointLimit(newPointLimit); - ConfigService.pointLimit = newPointLimit; }); }, ), ), + CustomFormRow( + prefixText: AppLocalizations.of(context).standard_mode, + prefixIcon: CupertinoIcons.square_stack, + suffixWidget: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + defaultMode == GameMode.none + ? AppLocalizations.of(context).no_default_mode + : (defaultMode == GameMode.pointLimit + ? '${ConfigService.getPointLimit()} ${AppLocalizations.of(context).points}' + : AppLocalizations.of(context).unlimited), + ), + const SizedBox(width: 5), + const CupertinoListTileChevron() + ], + ), + onPressed: () async { + final selectedMode = await Navigator.push( + context, + CupertinoPageRoute( + builder: (context) => ModeSelectionMenu( + pointLimit: ConfigService.getPointLimit(), + showDeselection: true, + ), + ), + ); + + setState(() { + defaultMode = selectedMode ?? GameMode.none; + }); + ConfigService.setGameMode(defaultMode); + }, + ), CustomFormRow( prefixText: AppLocalizations.of(context).reset_to_default, @@ -93,6 +127,7 @@ class _SettingsViewState extends State { setState(() { _stepperKey1 = UniqueKey(); _stepperKey2 = UniqueKey(); + defaultMode = ConfigService.getGameMode(); }); }, ) diff --git a/lib/presentation/views/tab_view.dart b/lib/presentation/views/tab_view.dart index 0c98cc7..08b1790 100644 --- a/lib/presentation/views/tab_view.dart +++ b/lib/presentation/views/tab_view.dart @@ -16,8 +16,9 @@ class _TabViewState extends State { @override Widget build(BuildContext context) { return CupertinoTabScaffold( + resizeToAvoidBottomInset: false, tabBar: CupertinoTabBar( - backgroundColor: CustomTheme.backgroundTintColor, + backgroundColor: CustomTheme.mainElementBackgroundColor, iconSize: 27, height: 55, items: [ diff --git a/lib/presentation/widgets/custom_button.dart b/lib/presentation/widgets/custom_button.dart new file mode 100644 index 0000000..0feb799 --- /dev/null +++ b/lib/presentation/widgets/custom_button.dart @@ -0,0 +1,19 @@ +import 'package:cabo_counter/core/custom_theme.dart'; +import 'package:flutter/cupertino.dart'; + +class CustomButton extends StatelessWidget { + final Widget child; + final VoidCallback? onPressed; + const CustomButton({super.key, required this.child, this.onPressed}); + + @override + Widget build(BuildContext context) { + return CupertinoButton( + sizeStyle: CupertinoButtonSize.medium, + borderRadius: BorderRadius.circular(12), + color: CustomTheme.buttonBackgroundColor, + onPressed: onPressed, + child: child, + ); + } +} diff --git a/lib/services/config_service.dart b/lib/services/config_service.dart index 70f6133..1b20ad5 100644 --- a/lib/services/config_service.dart +++ b/lib/services/config_service.dart @@ -1,3 +1,4 @@ +import 'package:cabo_counter/presentation/views/mode_selection_view.dart'; import 'package:shared_preferences/shared_preferences.dart'; /// This class handles the configuration settings for the app. @@ -6,53 +7,101 @@ import 'package:shared_preferences/shared_preferences.dart'; class ConfigService { static const String _keyPointLimit = 'pointLimit'; static const String _keyCaboPenalty = 'caboPenalty'; + static const String _keyGameMode = 'gameMode'; // Actual values used in the app - static int pointLimit = 100; - static int caboPenalty = 5; + static int _pointLimit = 100; + static int _caboPenalty = 5; + static int _gameMode = -1; // Default values static const int _defaultPointLimit = 100; static const int _defaultCaboPenalty = 5; + static const int _defaultGameMode = -1; static Future initConfig() async { final prefs = await SharedPreferences.getInstance(); - // Default values only set if they are not already set - prefs.setInt( - _keyPointLimit, prefs.getInt(_keyPointLimit) ?? _defaultPointLimit); - prefs.setInt( - _keyCaboPenalty, prefs.getInt(_keyCaboPenalty) ?? _defaultCaboPenalty); + // Initialize pointLimit, caboPenalty, and gameMode from SharedPreferences + // If they are not set, use the default values + _pointLimit = prefs.getInt(_keyPointLimit) ?? _defaultPointLimit; + _caboPenalty = prefs.getInt(_keyCaboPenalty) ?? _defaultCaboPenalty; + _gameMode = prefs.getInt(_keyGameMode) ?? _defaultGameMode; + + // Save the initial values to SharedPreferences + prefs.setInt(_keyPointLimit, _pointLimit); + prefs.setInt(_keyCaboPenalty, _caboPenalty); + prefs.setInt(_keyGameMode, _gameMode); } - /// Getter for the point limit. - static Future getPointLimit() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getInt(_keyPointLimit) ?? _defaultPointLimit; + /// Retrieves the current game mode. + /// + /// The game mode is determined based on the stored integer value: + /// - `0`: [GameMode.pointLimit] + /// - `1`: [GameMode.unlimited] + /// - Any other value: [GameMode.none] (-1 is used as a default for no mode) + /// + /// Returns the corresponding [GameMode] enum value. + static GameMode getGameMode() { + switch (_gameMode) { + case 0: + return GameMode.pointLimit; + case 1: + return GameMode.unlimited; + default: + return GameMode.none; + } } + /// Sets the game mode for the application. + /// + /// [newGameMode] is the new game mode to be set. It can be one of the following: + /// - `GameMode.pointLimit`: The game ends when a pleayer reaches the point limit. + /// - `GameMode.unlimited`: Every game goes for infinity until you end it. + /// - `GameMode.none`: No default mode set. + /// + /// This method updates the `_gameMode` field and persists the value in `SharedPreferences`. + static Future setGameMode(GameMode newGameMode) async { + int gameMode; + switch (newGameMode) { + case GameMode.pointLimit: + gameMode = 0; + break; + case GameMode.unlimited: + gameMode = 1; + break; + default: + gameMode = -1; + } + + final prefs = await SharedPreferences.getInstance(); + await prefs.setInt(_keyGameMode, gameMode); + _gameMode = gameMode; + } + + static int getPointLimit() => _pointLimit; + /// Setter for the point limit. /// [newPointLimit] is the new point limit to be set. static Future setPointLimit(int newPointLimit) async { final prefs = await SharedPreferences.getInstance(); await prefs.setInt(_keyPointLimit, newPointLimit); + _pointLimit = newPointLimit; } - /// Getter for the cabo penalty. - static Future getCaboPenalty() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getInt(_keyCaboPenalty) ?? _defaultCaboPenalty; - } + static int getCaboPenalty() => _caboPenalty; /// Setter for the cabo penalty. /// [newCaboPenalty] is the new cabo penalty to be set. static Future setCaboPenalty(int newCaboPenalty) async { final prefs = await SharedPreferences.getInstance(); await prefs.setInt(_keyCaboPenalty, newCaboPenalty); + _caboPenalty = newCaboPenalty; } /// Resets the configuration to default values. static Future resetConfig() async { - ConfigService.pointLimit = _defaultPointLimit; - ConfigService.caboPenalty = _defaultCaboPenalty; + ConfigService._pointLimit = _defaultPointLimit; + ConfigService._caboPenalty = _defaultCaboPenalty; + ConfigService._gameMode = _defaultGameMode; final prefs = await SharedPreferences.getInstance(); await prefs.setInt(_keyPointLimit, _defaultPointLimit); await prefs.setInt(_keyCaboPenalty, _defaultCaboPenalty); diff --git a/pubspec.yaml b/pubspec.yaml index 6773a53..2ea3c92 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: cabo_counter description: "Mobile app for the card game Cabo" publish_to: 'none' -version: 0.4.7+506 +version: 0.5.3+594 environment: sdk: ^3.5.4 @@ -28,6 +28,9 @@ dependencies: syncfusion_flutter_charts: ^30.1.37 uuid: ^4.5.1 rate_my_app: ^2.3.2 + reorderables: ^0.4.2 + collection: ^1.18.0 + confetti: ^0.6.0 dev_dependencies: flutter_test: diff --git a/test/data/game_session_test.dart b/test/data/game_session_test.dart index 4ca2158..9654bad 100644 --- a/test/data/game_session_test.dart +++ b/test/data/game_session_test.dart @@ -9,6 +9,7 @@ void main() { setUp(() { session = GameSession( + id: '1', createdAt: testDate, gameTitle: testTitle, players: testPlayers,