Implemented popup & confetti

This commit is contained in:
2025-07-20 22:36:55 +02:00
parent ddc2d68e9b
commit 5099dafbe9
2 changed files with 276 additions and 206 deletions

View File

@@ -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/points_view.dart';
import 'package:cabo_counter/presentation/views/round_view.dart'; import 'package:cabo_counter/presentation/views/round_view.dart';
import 'package:cabo_counter/services/local_storage_service.dart'; import 'package:cabo_counter/services/local_storage_service.dart';
import 'package:confetti/confetti.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -21,6 +22,9 @@ class ActiveGameView extends StatefulWidget {
} }
class _ActiveGameViewState extends State<ActiveGameView> { class _ActiveGameViewState extends State<ActiveGameView> {
final confettiController = ConfettiController(
duration: const Duration(seconds: 10),
);
late final GameSession gameSession; late final GameSession gameSession;
late List<int> denseRanks; late List<int> denseRanks;
late List<int> sortedPlayerIndices; late List<int> sortedPlayerIndices;
@@ -33,225 +37,258 @@ class _ActiveGameViewState extends State<ActiveGameView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListenableBuilder( return Stack(
listenable: gameSession, children: [
builder: (context, _) { ListenableBuilder(
sortedPlayerIndices = _getSortedPlayerIndices(); listenable: gameSession,
denseRanks = _calculateDenseRank( builder: (context, _) {
gameSession.playerScores, sortedPlayerIndices); sortedPlayerIndices = _getSortedPlayerIndices();
return CupertinoPageScaffold( denseRanks = _calculateDenseRank(
navigationBar: CupertinoNavigationBar( gameSession.playerScores, sortedPlayerIndices);
middle: Text(gameSession.gameTitle), return CupertinoPageScaffold(
), navigationBar: CupertinoNavigationBar(
child: SafeArea( middle: Text(gameSession.gameTitle),
child: SingleChildScrollView( ),
child: Column( child: SafeArea(
crossAxisAlignment: CrossAxisAlignment.start, child: SingleChildScrollView(
children: [ child: Column(
Padding( crossAxisAlignment: CrossAxisAlignment.start,
padding: const EdgeInsets.fromLTRB(10, 10, 0, 0), children: [
child: Text( Padding(
AppLocalizations.of(context).players, padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
style: CustomTheme.rowTitle, 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),
),
],
), ),
trailing: Row( ),
children: [ ListView.builder(
const SizedBox(width: 5), shrinkWrap: true,
Text('${gameSession.playerScores[playerIndex]} ' physics: const NeverScrollableScrollPhysics(),
'${AppLocalizations.of(context).points}') itemCount: gameSession.players.length,
], itemBuilder: (BuildContext context, int index) {
), int playerIndex = sortedPlayerIndices[index];
); return CupertinoListTile(
}, title: Row(
), children: [
Padding( _getPlacementTextWidget(index),
padding: const EdgeInsets.fromLTRB(10, 10, 0, 0), const SizedBox(width: 5),
child: Text( Text(
AppLocalizations.of(context).rounds, gameSession.players[playerIndex],
style: CustomTheme.rowTitle, style: const TextStyle(
), fontWeight: FontWeight.bold),
), ),
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: trailing: Row(
index + 1 != gameSession.roundNumber || 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 gameSession.isGameFinished == true
? (const Text('\u{2705}', ? (const Text('\u{2705}',
style: TextStyle(fontSize: 22))) style: TextStyle(fontSize: 22)))
: const Text('\u{23F3}', : const Text('\u{23F3}',
style: TextStyle(fontSize: 22)), style: TextStyle(fontSize: 22)),
onTap: () async { onTap: () async {
_openRoundView(index + 1); _openRoundView(context, index + 1);
}, },
)); ));
}, },
), ),
Padding( Padding(
padding: const EdgeInsets.fromLTRB(10, 10, 0, 0), padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
child: Text( child: Text(
AppLocalizations.of(context).statistics, AppLocalizations.of(context).statistics,
style: CustomTheme.rowTitle, style: CustomTheme.rowTitle,
), ),
), ),
Column( Column(
children: [ children: [
CupertinoListTile( CupertinoListTile(
title: Text( title: Text(
AppLocalizations.of(context).scoring_history, 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: CupertinoListTile(
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( title: Text(
AppLocalizations.of(context).end_game, AppLocalizations.of(context).delete_game,
style: gameSession.roundNumber > 1 &&
!gameSession.isGameFinished
? const TextStyle(color: Colors.white)
: const TextStyle(color: Colors.white30),
), ),
backgroundColorActivated: backgroundColorActivated:
CustomTheme.backgroundColor, CustomTheme.backgroundColor,
onTap: () { onTap: () {
if (gameSession.roundNumber > 1 && _showDeleteGameDialog().then((value) {
!gameSession.isGameFinished) { if (value) {
_showEndGameDialog(); _removeGameSession(gameSession);
} }
}), });
), },
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,
), ),
backgroundColorActivated: CupertinoListTile(
CustomTheme.backgroundColor, title: Text(
onTap: () async { AppLocalizations.of(context)
final success = await LocalStorageService .new_game_same_settings,
.exportSingleGameSession( ),
widget.gameSession); backgroundColorActivated:
if (!success && context.mounted) { CustomTheme.backgroundColor,
showCupertinoDialog( onTap: () {
context: context, Navigator.pushReplacement(
builder: (context) => CupertinoAlertDialog( context,
title: Text(AppLocalizations.of(context) CupertinoPageRoute(
.export_error_title), builder: (_) => CreateGameView(
content: Text(AppLocalizations.of(context) gameTitle:
.export_error_message), gameSession.gameTitle,
actions: [ gameMode: widget.gameSession
CupertinoDialogAction( .isPointsLimitEnabled ==
child: Text( true
AppLocalizations.of(context).ok), ? GameMode.pointLimit
onPressed: () => : GameMode.unlimited,
Navigator.pop(context), 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. /// Shows a dialog to confirm ending the game.
@@ -403,7 +440,7 @@ class _ActiveGameViewState extends State<ActiveGameView> {
/// Recursively opens the RoundView for the specified round number. /// Recursively opens the RoundView for the specified round number.
/// It starts with the given [roundNumber] and continues to open the next round /// It starts with the given [roundNumber] and continues to open the next round
/// until the user navigates back or the round number is invalid. /// 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( final val = await Navigator.of(context, rootNavigator: true).push(
CupertinoPageRoute( CupertinoPageRoute(
fullscreenDialog: true, fullscreenDialog: true,
@@ -413,10 +450,42 @@ class _ActiveGameViewState extends State<ActiveGameView> {
), ),
), ),
); );
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) { if (val != null && val >= 0) {
WidgetsBinding.instance.addPostFrameCallback((_) async { WidgetsBinding.instance.addPostFrameCallback((_) async {
await Future.delayed(const Duration(milliseconds: 600)); await Future.delayed(const Duration(milliseconds: 600));
_openRoundView(val); if (context.mounted) {
_openRoundView(context, val);
}
}); });
} }
} }

View File

@@ -2,7 +2,7 @@ name: cabo_counter
description: "Mobile app for the card game Cabo" description: "Mobile app for the card game Cabo"
publish_to: 'none' publish_to: 'none'
version: 0.5.1+568 version: 0.5.2+579
environment: environment:
sdk: ^3.5.4 sdk: ^3.5.4
@@ -30,6 +30,7 @@ dependencies:
rate_my_app: ^2.3.2 rate_my_app: ^2.3.2
reorderables: ^0.4.2 reorderables: ^0.4.2
collection: ^1.18.0 collection: ^1.18.0
confetti: ^0.6.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: