Merge pull request #127 from flixcoo/feature/126-popup-for-winner

Popup for winner
This commit is contained in:
2025-07-20 23:40:01 +02:00
committed by GitHub
12 changed files with 572 additions and 410 deletions

View File

@@ -19,4 +19,13 @@ class Constants {
remindDays: 45, remindDays: 45,
minLaunches: 15, minLaunches: 15,
remindLaunches: 40); 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;
} }

View File

@@ -299,15 +299,19 @@ class GameSession extends ChangeNotifier {
/// It iterates through the player scores and finds the player /// It iterates through the player scores and finds the player
/// with the lowest score. /// with the lowest score.
void _setWinner() { void _setWinner() {
int score = playerScores[0]; int minScore = playerScores.reduce((a, b) => a < b ? a : b);
String lowestPlayer = players[0]; List<String> lowestPlayers = [];
for (int i = 0; i < players.length; i++) { for (int i = 0; i < players.length; i++) {
if (playerScores[i] < score) { if (playerScores[i] == minScore) {
score = playerScores[i]; lowestPlayers.add(players[i]);
lowestPlayer = players[i];
} }
} }
winner = lowestPlayer; if (lowestPlayers.length > 1) {
winner =
'${lowestPlayers.sublist(0, lowestPlayers.length - 1).join(', ')} & ${lowestPlayers.last}';
} else {
winner = lowestPlayers.first;
}
notifyListeners(); notifyListeners();
} }

View File

@@ -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", "end_game": "Spiel beenden",
"delete_game": "Spiel löschen", "delete_game": "Spiel löschen",
"new_game_same_settings": "Neues Spiel mit gleichen Einstellungen", "new_game_same_settings": "Neues Spiel mit gleichen Einstellungen",

View File

@@ -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", "end_game": "End Game",
"delete_game": "Delete Game", "delete_game": "Delete Game",
"new_game_same_settings": "New Game with same Settings", "new_game_same_settings": "New Game with same Settings",

View File

@@ -453,6 +453,18 @@ abstract class AppLocalizations {
String bonus_points_message( String bonus_points_message(
int playerCount, String names, int pointLimit, int bonusPoints); 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. /// No description provided for @end_game.
/// ///
/// In de, this message translates to: /// In de, this message translates to:

View File

@@ -208,6 +208,21 @@ class AppLocalizationsDe extends AppLocalizations {
return '$_temp0'; 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 @override
String get end_game => 'Spiel beenden'; String get end_game => 'Spiel beenden';

View File

@@ -205,6 +205,14 @@ class AppLocalizationsEn extends AppLocalizations {
return '$_temp0'; 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 @override
String get end_game => 'End Game'; String get end_game => 'End Game';

View File

@@ -1,3 +1,4 @@
import 'package:cabo_counter/core/constants.dart';
import 'package:cabo_counter/core/custom_theme.dart'; import 'package:cabo_counter/core/custom_theme.dart';
import 'package:cabo_counter/data/game_manager.dart'; import 'package:cabo_counter/data/game_manager.dart';
import 'package:cabo_counter/data/game_session.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/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:collection/collection.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 +24,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 +39,254 @@ 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),
),
],
), ),
], );
), }
); }),
} ],
}), )
], ],
) ),
], ),
), ));
), }),
)); 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,8 +438,8 @@ 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 round = await Navigator.of(context, rootNavigator: true).push(
CupertinoPageRoute( CupertinoPageRoute(
fullscreenDialog: true, fullscreenDialog: true,
builder: (context) => RoundView( builder: (context) => RoundView(
@@ -413,11 +448,58 @@ class _ActiveGameViewState extends State<ActiveGameView> {
), ),
), ),
); );
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 { WidgetsBinding.instance.addPostFrameCallback((_) async {
await Future.delayed(const Duration(milliseconds: 600)); await Future.delayed(
_openRoundView(val); 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<void> _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();
}
} }

View File

@@ -1,3 +1,4 @@
import 'package:cabo_counter/core/constants.dart';
import 'package:cabo_counter/core/custom_theme.dart'; import 'package:cabo_counter/core/custom_theme.dart';
import 'package:cabo_counter/data/game_manager.dart'; import 'package:cabo_counter/data/game_manager.dart';
import 'package:cabo_counter/data/game_session.dart'; import 'package:cabo_counter/data/game_session.dart';
@@ -187,7 +188,8 @@ class _CreateGameViewState extends State<CreateGameView> {
opacity: _playerNameTextControllers.length > 1 opacity: _playerNameTextControllers.length > 1
? 1.0 ? 1.0
: 0.0, : 0.0,
duration: const Duration(milliseconds: 300), duration: const Duration(
milliseconds: Constants.fadeInDuration),
child: Padding( child: Padding(
padding: const EdgeInsets.only(right: 8.0), padding: const EdgeInsets.only(right: 8.0),
child: ReorderableDragStartListener( child: ReorderableDragStartListener(

View File

@@ -30,55 +30,58 @@ class _GraphViewState extends State<GraphView> {
middle: Text(AppLocalizations.of(context).scoring_history), middle: Text(AppLocalizations.of(context).scoring_history),
previousPageTitle: AppLocalizations.of(context).back, previousPageTitle: AppLocalizations.of(context).back,
), ),
child: widget.gameSession.roundNumber > 1 child: Visibility(
? Padding( visible: widget.gameSession.roundNumber > 1 ||
padding: const EdgeInsets.fromLTRB(0, 100, 0, 0), widget.gameSession.isGameFinished,
child: SfCartesianChart( replacement: Column(
enableAxisAnimation: true, mainAxisAlignment: MainAxisAlignment.center,
legend: const Legend( crossAxisAlignment: CrossAxisAlignment.center,
overflowMode: LegendItemOverflowMode.wrap, children: [
isVisible: true, const Center(
position: LegendPosition.bottom), child: Icon(CupertinoIcons.chart_bar_alt_fill, size: 60),
primaryXAxis: const NumericAxis( ),
labelStyle: TextStyle(fontWeight: FontWeight.bold), const SizedBox(height: 10),
interval: 1, Padding(
decimalPlaces: 0, padding: const EdgeInsets.symmetric(horizontal: 40),
), child: Text(
primaryYAxis: NumericAxis( AppLocalizations.of(context).empty_graph_text,
labelStyle: const TextStyle(fontWeight: FontWeight.bold), textAlign: TextAlign.center,
labelAlignment: LabelAlignment.center, style: const TextStyle(fontSize: 16),
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(),
), ),
) ),
: Column( ],
mainAxisAlignment: MainAxisAlignment.center, ),
crossAxisAlignment: CrossAxisAlignment.center, child: Padding(
children: [ padding: const EdgeInsets.fromLTRB(0, 100, 0, 0),
const Center( child: SfCartesianChart(
child: Icon(CupertinoIcons.chart_bar_alt_fill, size: 60), enableAxisAnimation: true,
), legend: const Legend(
const SizedBox(height: 10), overflowMode: LegendItemOverflowMode.wrap,
Padding( isVisible: true,
padding: const EdgeInsets.symmetric(horizontal: 40), position: LegendPosition.bottom),
child: Text( primaryXAxis: const NumericAxis(
AppLocalizations.of(context).empty_graph_text, labelStyle: TextStyle(fontWeight: FontWeight.bold),
textAlign: TextAlign.center, interval: 1,
style: const TextStyle(fontSize: 16), 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. /// Returns a list of LineSeries representing the cumulative scores of each player.

View File

@@ -58,160 +58,156 @@ class _MainMenuViewState extends State<MainMenuView> {
listenable: gameManager, listenable: gameManager,
builder: (context, _) { builder: (context, _) {
return CupertinoPageScaffold( return CupertinoPageScaffold(
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
navigationBar: CupertinoNavigationBar( navigationBar: CupertinoNavigationBar(
leading: IconButton( leading: IconButton(
onPressed: () { onPressed: () {
Navigator.push( 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(
context, context,
CupertinoPageRoute( CupertinoPageRoute(
builder: (context) => CreateGameView( builder: (context) => const SettingsView(),
gameMode: ConfigService.getGameMode()),
), ),
).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)), itemBuilder: (context, index) {
), final session = gameManager.gameList[index];
child: CupertinoPageScaffold( return ListenableBuilder(
child: SafeArea( listenable: session,
child: _isLoading builder: (context, _) {
? const Center(child: CupertinoActivityIndicator()) return Dismissible(
: gameManager.gameList.isEmpty key: Key(session.id),
? Column( background: Container(
mainAxisAlignment: MainAxisAlignment.center, color: CupertinoColors.destructiveRed,
children: [ alignment: Alignment.centerRight,
const SizedBox(height: 30), padding: const EdgeInsets.only(right: 20.0),
Center( child: const Icon(
child: GestureDetector( CupertinoIcons.delete,
onTap: () => Navigator.push( color: CupertinoColors.white,
context,
CupertinoPageRoute(
builder: (context) => CreateGameView(
gameMode: ConfigService.getGameMode()),
), ),
), ),
child: Icon( direction: DismissDirection.endToStart,
CupertinoIcons.plus, confirmDismiss: (direction) async {
size: 60, return await _showDeleteGamePopup(
color: CustomTheme.primaryColor, 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: Column(
child: Text( mainAxisAlignment: MainAxisAlignment.center,
'${AppLocalizations.of(context).empty_text_1}\n${AppLocalizations.of(context).empty_text_2}', children: [
textAlign: TextAlign.center, const SizedBox(height: 30),
style: const TextStyle(fontSize: 16), Center(
), child: GestureDetector(
), onTap: () => Navigator.push(
], context,
) CupertinoPageRoute(
: ListView.separated( builder: (context) => CreateGameView(
itemCount: gameManager.gameList.length, gameMode: ConfigService.getGameMode()),
separatorBuilder: (context, index) => Divider(
height: 1,
thickness: 0.5,
color: CustomTheme.white.withAlpha(50),
indent: 50,
endIndent: 50,
), ),
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<MainMenuView> {
BadRatingDialogDecision badRatingDecision = BadRatingDialogDecision.cancel; BadRatingDialogDecision badRatingDecision = BadRatingDialogDecision.cancel;
// so that the bad rating dialog is not shown immediately // 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) { switch (preRatingDecision) {
case PreRatingDialogDecision.yes: case PreRatingDialogDecision.yes:

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+581
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: