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/data/game_session.dart b/lib/data/game_session.dart index a9164be..3dcf4c2 100644 --- a/lib/data/game_session.dart +++ b/lib/data/game_session.dart @@ -299,15 +299,19 @@ class GameSession extends ChangeNotifier { /// It iterates through the player scores and finds the player /// with the lowest score. void _setWinner() { - int score = playerScores[0]; - String lowestPlayer = players[0]; + 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 effbd96..cd1a8c7 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -96,6 +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", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index ea169e9..bedf95f 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -96,6 +96,21 @@ } }, + "end_of_game_title": "End of Game", + "end_of_game_message": "{names} won the game with {points} points. Congratulations!", + "@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", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 9c64cb2..5749079 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -453,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: diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index c4849d8..87745dd 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -208,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'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 6b2689e..837c83e 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -205,6 +205,14 @@ 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) { + return '$names won the game with $points points. Congratulations!'; + } + @override String get end_game => 'End Game'; diff --git a/lib/presentation/views/active_game_view.dart b/lib/presentation/views/active_game_view.dart index dece0a6..25e88a7 100644 --- a/lib/presentation/views/active_game_view.dart +++ b/lib/presentation/views/active_game_view.dart @@ -1,3 +1,4 @@ +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'; @@ -8,6 +9,8 @@ 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'; @@ -21,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; @@ -33,225 +39,254 @@ 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), + ), + 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).statistics, - style: CustomTheme.rowTitle, - ), - ), - Column( - children: [ - CupertinoListTile( - title: Text( - AppLocalizations.of(context).scoring_history, + 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, - )))), - 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( + 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, - gameMode: widget.gameSession - .isPointsLimitEnabled == - true - ? GameMode.pointLimit - : GameMode.unlimited, - 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. @@ -403,8 +438,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( @@ -413,11 +448,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 870ea29..236baff 100644 --- a/lib/presentation/views/create_game_view.dart +++ b/lib/presentation/views/create_game_view.dart @@ -1,3 +1,4 @@ +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'; @@ -187,7 +188,8 @@ class _CreateGameViewState extends State { opacity: _playerNameTextControllers.length > 1 ? 1.0 : 0.0, - duration: const Duration(milliseconds: 300), + duration: const Duration( + milliseconds: Constants.fadeInDuration), child: Padding( padding: const EdgeInsets.only(right: 8.0), child: ReorderableDragStartListener( diff --git a/lib/presentation/views/graph_view.dart b/lib/presentation/views/graph_view.dart index 23137cd..da9f8e7 100644 --- a/lib/presentation/views/graph_view.dart +++ b/lib/presentation/views/graph_view.dart @@ -30,55 +30,58 @@ class _GraphViewState extends State { 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( - 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(), + 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 f8817a0..6d8262b 100644 --- a/lib/presentation/views/main_menu_view.dart +++ b/lib/presentation/views/main_menu_view.dart @@ -58,160 +58,156 @@ 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: Text(AppLocalizations.of(context).app_name), - trailing: IconButton( - onPressed: () => Navigator.push( + resizeToAvoidBottomInset: false, + navigationBar: CupertinoNavigationBar( + leading: IconButton( + onPressed: () { + Navigator.push( context, CupertinoPageRoute( - builder: (context) => CreateGameView( - gameMode: ConfigService.getGameMode()), + 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) => CreateGameView( - gameMode: ConfigService.getGameMode()), + 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, + 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), + ), + child: Text( + '\u{1F947} ${session.winner}', + 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(() {}); + }); + }, + ), ), - )), - 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.separated( - itemCount: gameManager.gameList.length, - separatorBuilder: (context, index) => Divider( - height: 1, - thickness: 0.5, - color: CustomTheme.white.withAlpha(50), - indent: 50, - endIndent: 50, + ); + }); + }, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 30), + Center( + child: GestureDetector( + onTap: () => Navigator.push( + context, + CupertinoPageRoute( + builder: (context) => CreateGameView( + gameMode: ConfigService.getGameMode()), ), - 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, - ), - ), - 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: - 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: 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()), + ), + ))); }); } @@ -243,7 +239,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/pubspec.yaml b/pubspec.yaml index c9be09f..01d641d 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.5.1+568 +version: 0.5.2+581 environment: sdk: ^3.5.4 @@ -30,6 +30,7 @@ dependencies: rate_my_app: ^2.3.2 reorderables: ^0.4.2 collection: ^1.18.0 + confetti: ^0.6.0 dev_dependencies: flutter_test: