diff --git a/lib/presentation/views/active_game_view.dart b/lib/presentation/views/active_game_view.dart index dece0a6..a14194b 100644 --- a/lib/presentation/views/active_game_view.dart +++ b/lib/presentation/views/active_game_view.dart @@ -8,6 +8,7 @@ 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:confetti/confetti.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -21,6 +22,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 +37,258 @@ 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), + ), + ], ), - ], - ), - ); - } - }), + ); + } + }), + CupertinoListTile( + title: const Text('Konfetti'), + onTap: () => confettiController.play(), + ) + ], + ) ], - ) - ], - ), - ), - )); - }); + ), + ), + )); + }), + 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,7 +440,7 @@ 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 { + void _openRoundView(BuildContext context, int roundNumber) async { final val = await Navigator.of(context, rootNavigator: true).push( CupertinoPageRoute( fullscreenDialog: true, @@ -413,10 +450,42 @@ class _ActiveGameViewState extends State { ), ), ); + if (widget.gameSession.isGameFinished && mounted) { + String winner = widget.gameSession.winner; + int winnerIndex = widget.gameSession.players.indexOf(winner); + int points = widget.gameSession.playerScores[winnerIndex]; + + confettiController.play(); + + await Future.delayed(const Duration(milliseconds: 300)); + + 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(1, winner, points)), + actions: [ + CupertinoDialogAction( + child: Text(AppLocalizations.of(context).ok), + onPressed: () { + confettiController.stop(); + Navigator.pop(context); + }, + ), + ], + ); + }); + } + } if (val != null && val >= 0) { WidgetsBinding.instance.addPostFrameCallback((_) async { await Future.delayed(const Duration(milliseconds: 600)); - _openRoundView(val); + if (context.mounted) { + _openRoundView(context, val); + } }); } } diff --git a/pubspec.yaml b/pubspec.yaml index c9be09f..06a051a 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+579 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: