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
This commit is contained in:
2025-07-21 13:29:25 +02:00
committed by GitHub
parent c19ce71198
commit d627f33579
24 changed files with 1503 additions and 799 deletions

View File

@@ -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<ActiveGameView> {
final confettiController = ConfettiController(
duration: const Duration(seconds: 10),
);
late final GameSession gameSession;
late List<int> denseRanks;
late List<int> sortedPlayerIndices;
@@ -31,200 +39,257 @@ class _ActiveGameViewState extends State<ActiveGameView> {
@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<ActiveGameView> {
onPressed: () {
setState(() {
gameManager.endGame(gameSession.id);
_playFinishAnimation(context);
});
Navigator.pop(context);
},
@@ -376,8 +442,8 @@ class _ActiveGameViewState extends State<ActiveGameView> {
/// 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<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 {
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<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,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<String>? 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<CreateGameView> {
final TextEditingController _gameTitleTextController =
TextEditingController();
/// List of text controllers for player names.
final List<TextEditingController> _playerNameTextControllers = [
TextEditingController()
];
final TextEditingController _gameTitleTextController =
TextEditingController();
/// List of focus nodes for player name text fields.
final List<FocusNode> _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<CreateGameView> {
@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<CreateGameView> {
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<String> 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<CreateGameView> {
}
}
/// 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<String> 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<CreateGameView> {
for (var controller in _playerNameTextControllers) {
controller.dispose();
}
for (var focusnode in _playerNameFocusNodes) {
focusnode.dispose();
}
super.dispose();
}

View File

@@ -27,46 +27,61 @@ class _GraphViewState extends State<GraphView> {
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.

View File

@@ -58,162 +58,167 @@ class _MainMenuViewState extends State<MainMenuView> {
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<MainMenuView> {
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:

View File

@@ -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);
},
),
)),
],
),
);

View File

@@ -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<PointsView> createState() => _PointsViewState();
}
class _PointsViewState extends State<PointsView> {
@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<DataRow>.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),
),
),
),
),
],
),
],
),
),
),
);
}
}

View File

@@ -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<RoundView> {
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<RoundView> {
vertical: 10,
),
child: SizedBox(
height: 40,
height: 60,
child: CupertinoSegmentedControl<int>(
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<RoundView> {
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 6,
vertical: 8,
),
child: FittedBox(
fit: BoxFit.scaleDown,
@@ -154,27 +157,6 @@ class _RoundViewState extends State<RoundView> {
),
),
),
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<RoundView> {
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<RoundView> {
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<RoundView> {
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<int> 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<RoundView> {
if (!widget.gameSession.isGameFinished)
CupertinoButton(
onPressed: _areRoundInputsValid()
? () async {
List<int> 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<RoundView> {
];
}
/// 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<bool> _showKamikazeSheet(BuildContext context) async {
return await showCupertinoModalPopup<bool?>(
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<RoundView> {
return bonusPlayers;
}
/// Shows a popup dialog with the bonus information.
/// Shows a popup dialog with the information which player received the bonus points.
Future<void> _showBonusPopup(
BuildContext context, List<int> 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<RoundView> {
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<void> _endOfRoundNavigation(
BuildContext context, bool navigateToNextRound) async {
List<int> 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) {

View File

@@ -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<SettingsView> {
UniqueKey _stepperKey1 = UniqueKey();
UniqueKey _stepperKey2 = UniqueKey();
GameMode defaultMode = ConfigService.getGameMode();
@override
void initState() {
super.initState();
@@ -55,14 +57,13 @@ class _SettingsViewState extends State<SettingsView> {
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<SettingsView> {
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<SettingsView> {
setState(() {
_stepperKey1 = UniqueKey();
_stepperKey2 = UniqueKey();
defaultMode = ConfigService.getGameMode();
});
},
)

View File

@@ -16,8 +16,9 @@ class _TabViewState extends State<TabView> {
@override
Widget build(BuildContext context) {
return CupertinoTabScaffold(
resizeToAvoidBottomInset: false,
tabBar: CupertinoTabBar(
backgroundColor: CustomTheme.backgroundTintColor,
backgroundColor: CustomTheme.mainElementBackgroundColor,
iconSize: 27,
height: 55,
items: <BottomNavigationBarItem>[