Files
cabo-counter/lib/presentation/views/active_game_view.dart

424 lines
18 KiB
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:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class ActiveGameView extends StatefulWidget {
final GameSession gameSession;
const ActiveGameView({super.key, required this.gameSession});
@override
// ignore: library_private_types_in_public_api
_ActiveGameViewState createState() => _ActiveGameViewState();
}
class _ActiveGameViewState extends State<ActiveGameView> {
late final GameSession gameSession;
late List<int> denseRanks;
late List<int> sortedPlayerIndices;
@override
void initState() {
super.initState();
gameSession = widget.gameSession;
}
@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),
),
],
),
trailing: Row(
children: [
const SizedBox(width: 5),
Text('${gameSession.playerScores[playerIndex]} '
'${AppLocalizations.of(context).points}')
],
),
);
},
),
Padding(
padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
child: Text(
AppLocalizations.of(context).rounds,
style: CustomTheme.rowTitle,
),
),
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: gameSession.roundNumber,
itemBuilder: (BuildContext context, int index) {
return Padding(
padding: const EdgeInsets.all(1),
child: CupertinoListTile(
backgroundColorActivated:
CustomTheme.backgroundColor,
title: Text(
'${AppLocalizations.of(context).round} ${index + 1}',
),
trailing:
index + 1 != gameSession.roundNumber ||
gameSession.isGameFinished == true
? (const Text('\u{2705}',
style: TextStyle(fontSize: 22)))
: const Text('\u{23F3}',
style: TextStyle(fontSize: 22)),
onTap: () async {
_openRoundView(index + 1);
},
));
},
),
Padding(
padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
child: Text(
AppLocalizations.of(context).statistics,
style: CustomTheme.rowTitle,
),
),
Column(
children: [
CupertinoListTile(
title: Text(
AppLocalizations.of(context).scoring_history,
),
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();
}
}),
),
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:
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),
),
],
),
);
}
}),
],
)
],
),
),
));
});
}
/// Shows a dialog to confirm ending the game.
/// If the user confirms, it calls the `endGame` method on the game manager
void _showEndGameDialog() {
showCupertinoDialog(
context: context,
builder: (BuildContext context) {
return CupertinoAlertDialog(
title: Text(AppLocalizations.of(context).end_game_title),
content: Text(AppLocalizations.of(context).end_game_message),
actions: [
CupertinoDialogAction(
child: Text(
AppLocalizations.of(context).end_game,
style: const TextStyle(
fontWeight: FontWeight.bold,
color: CupertinoColors.destructiveRed),
),
onPressed: () {
setState(() {
gameManager.endGame(gameSession.id);
});
Navigator.pop(context);
},
),
CupertinoDialogAction(
child: Text(AppLocalizations.of(context).cancel),
onPressed: () => Navigator.pop(context),
),
],
);
},
);
}
/// Returns a list of player indices sorted by their scores in
/// ascending order.
List<int> _getSortedPlayerIndices() {
List<int> playerIndices =
List<int>.generate(gameSession.players.length, (index) => index);
// Sort the indices based on the summed points
playerIndices.sort((a, b) {
int scoreA = gameSession.playerScores[a];
int scoreB = gameSession.playerScores[b];
if (scoreA != scoreB) {
return scoreA.compareTo(scoreB);
}
return a.compareTo(b);
});
return playerIndices;
}
/// Calculates the dense rank for a player based on their index in the sorted list of players.
List<int> _calculateDenseRank(
List<int> playerScores, List<int> sortedIndices) {
List<int> denseRanks = [];
int rank = 1;
for (int i = 0; i < sortedIndices.length; i++) {
if (i > 0) {
int prevScore = playerScores[sortedIndices[i - 1]];
int currScore = playerScores[sortedIndices[i]];
if (currScore != prevScore) {
rank++;
}
}
denseRanks.add(rank);
}
return denseRanks;
}
/// Returns a text widget representing the placement text based on the given placement number.
/// [index] is the index of the player in [players] list,
Text _getPlacementTextWidget(int index) {
int placement = denseRanks[index];
switch (placement) {
case 1:
return const Text('\u{1F947}', style: TextStyle(fontSize: 22)); // 🥇
case 2:
return const Text('\u{1F948}', style: TextStyle(fontSize: 22)); // 🥈
case 3:
return const Text('\u{1F949}', style: TextStyle(fontSize: 22)); // 🥉
default:
return Text(' $placement.',
style: const TextStyle(fontWeight: FontWeight.bold));
}
}
/// Shows a dialog to confirm deleting the game session.
Future<bool> _showDeleteGameDialog() async {
return await showCupertinoDialog<bool>(
context: context,
builder: (BuildContext context) {
return CupertinoAlertDialog(
title: Text(AppLocalizations.of(context).delete_game_title),
content: Text(
AppLocalizations.of(context)
.delete_game_message(gameSession.gameTitle),
),
actions: [
CupertinoDialogAction(
child: Text(AppLocalizations.of(context).cancel),
onPressed: () => Navigator.pop(context, false),
),
CupertinoDialogAction(
child: Text(
AppLocalizations.of(context).delete,
style: const TextStyle(
fontWeight: FontWeight.bold, color: Colors.red),
),
onPressed: () {
Navigator.pop(context, true);
},
),
],
);
},
) ??
false;
}
/// Removes the game session in the game manager and navigates back to the previous screen.
/// If the game session does not exist in the game list, it shows an error dialog.
Future<void> _removeGameSession(GameSession gameSession) async {
if (gameManager.gameExistsInGameList(gameSession.id)) {
Navigator.pop(context);
WidgetsBinding.instance.addPostFrameCallback((_) {
gameManager.removeGameSessionById(gameSession.id);
});
} else {
showCupertinoDialog(
context: context,
builder: (BuildContext context) {
return CupertinoAlertDialog(
title: Text(AppLocalizations.of(context).id_error_title),
content: Text(AppLocalizations.of(context).id_error_message),
actions: [
CupertinoDialogAction(
child: Text(AppLocalizations.of(context).ok),
onPressed: () => Navigator.pop(context),
),
],
);
});
}
}
/// 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(
CupertinoPageRoute(
fullscreenDialog: true,
builder: (context) => RoundView(
gameSession: gameSession,
roundNumber: roundNumber,
),
),
);
if (val != null && val >= 0) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
await Future.delayed(const Duration(milliseconds: 600));
_openRoundView(val);
});
}
}
}